name: Build on: workflow_call: inputs: upload_artifacts: required: false type: boolean default: false description: 'Whether to upload build artifacts' secrets: MAC_CERT_P12_BASE64: required: false MAC_CERT_P12_PASSWORD: required: false APPLE_API_KEY_ID: required: false APPLE_API_ISSUER: required: false APPLE_API_KEY_P8_BASE64: required: false jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: include: - platform: windows os: windows-latest build_script: pnpm run build:win ytdlp_asset: yt-dlp.exe ytdlp_output: yt-dlp.exe ffmpeg_url: https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-win64-gpl.zip ffmpeg_inner_path: ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe ffprobe_inner_path: ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe - platform: macos os: macos-latest build_script: pnpm run build:mac ytdlp_asset: yt-dlp_macos ytdlp_output: yt-dlp_macos ffmpeg_arm_url: https://github.com/eko5624/mpv-mac/releases/download/2026-01-12/ffmpeg-arm64-96e8f3b8cc.zip ffmpeg_x86_url: https://github.com/eko5624/mpv-mac/releases/download/2026-01-12/ffmpeg-x86_64-96e8f3b8cc.zip ffmpeg_inner_path: ffmpeg/ffmpeg ffprobe_inner_path: ffmpeg/ffprobe - platform: linux os: ubuntu-latest build_script: pnpm run build:linux ytdlp_asset: yt-dlp ytdlp_output: yt-dlp_linux ffmpeg_url: https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-linux64-gpl.tar.xz ffmpeg_inner_path: ffmpeg-master-latest-linux64-gpl/bin/ffmpeg ffprobe_inner_path: ffmpeg-master-latest-linux64-gpl/bin/ffprobe steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 8 - name: Install Dependencies run: pnpm install - name: Download ffmpeg binary (Windows) if: matrix.platform == 'windows' shell: pwsh run: | $ErrorActionPreference = 'Stop' $ffmpegUrl = '${{ matrix.ffmpeg_url }}' Invoke-WebRequest -Uri $ffmpegUrl -OutFile ffmpeg.zip Expand-Archive ffmpeg.zip -DestinationPath ffmpeg -Force $source = Join-Path 'ffmpeg' '${{ matrix.ffmpeg_inner_path }}' $ffprobeSource = Join-Path 'ffmpeg' '${{ matrix.ffprobe_inner_path }}' $destinationDir = Join-Path 'resources' 'ffmpeg' New-Item -ItemType Directory -Path $destinationDir -Force | Out-Null Copy-Item -Path $source -Destination (Join-Path $destinationDir 'ffmpeg.exe') -Force Copy-Item -Path $ffprobeSource -Destination (Join-Path $destinationDir 'ffprobe.exe') -Force Remove-Item ffmpeg.zip -Force Remove-Item ffmpeg -Recurse -Force - name: Download ffmpeg binary (macOS) if: matrix.platform == 'macos' shell: bash env: FFMPEG_OUTPUT: ffmpeg FFPROBE_OUTPUT: ffprobe run: | set -euo pipefail curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_arm_url }}" -o ffmpeg-arm.zip unzip -q ffmpeg-arm.zip -d ffmpeg-arm curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_x86_url }}" -o ffmpeg-x86.zip unzip -q ffmpeg-x86.zip -d ffmpeg-x86 arm_bin="ffmpeg-arm/${{ matrix.ffmpeg_inner_path }}" x86_bin="ffmpeg-x86/${{ matrix.ffmpeg_inner_path }}" arm_probe="ffmpeg-arm/${{ matrix.ffprobe_inner_path }}" x86_probe="ffmpeg-x86/${{ matrix.ffprobe_inner_path }}" if [[ ! -f "$arm_bin" ]]; then arm_bin="$(find ffmpeg-arm -type f -name ffmpeg -print -quit)" fi if [[ ! -f "$x86_bin" ]]; then x86_bin="$(find ffmpeg-x86 -type f -name ffmpeg -print -quit)" fi if [[ ! -f "$arm_probe" ]]; then arm_probe="$(find ffmpeg-arm -type f -name ffprobe -print -quit)" fi if [[ ! -f "$x86_probe" ]]; then x86_probe="$(find ffmpeg-x86 -type f -name ffprobe -print -quit)" fi if [[ ! -f "$arm_bin" ]]; then echo "::error::Missing arm64 ffmpeg binary at $arm_bin" exit 1 fi if [[ ! -f "$x86_bin" ]]; then echo "::error::Missing x86_64 ffmpeg binary at $x86_bin" exit 1 fi if [[ ! -f "$arm_probe" ]]; then echo "::error::Missing arm64 ffprobe binary at $arm_probe" exit 1 fi if [[ ! -f "$x86_probe" ]]; then echo "::error::Missing x86_64 ffprobe binary at $x86_probe" exit 1 fi mkdir -p resources/ffmpeg lipo -create "$arm_bin" "$x86_bin" -output "resources/ffmpeg/$FFMPEG_OUTPUT" lipo -create "$arm_probe" "$x86_probe" -output "resources/ffmpeg/$FFPROBE_OUTPUT" chmod +x "resources/ffmpeg/$FFMPEG_OUTPUT" "resources/ffmpeg/$FFPROBE_OUTPUT" rm -rf ffmpeg-arm ffmpeg-x86 ffmpeg-arm.zip ffmpeg-x86.zip - name: Download ffmpeg binary (Linux) if: matrix.platform == 'linux' shell: bash run: | set -euo pipefail curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_url }}" -o ffmpeg.tar.xz if ! tar -tf ffmpeg.tar.xz >/dev/null 2>&1; then echo "::error::Downloaded ffmpeg archive is not a valid tar.xz" exit 1 fi mkdir ffmpeg tar -xf ffmpeg.tar.xz -C ffmpeg mkdir -p resources/ffmpeg cp "ffmpeg/${{ matrix.ffmpeg_inner_path }}" "resources/ffmpeg/ffmpeg" cp "ffmpeg/${{ matrix.ffprobe_inner_path }}" "resources/ffmpeg/ffprobe" chmod +x "resources/ffmpeg/ffmpeg" "resources/ffmpeg/ffprobe" rm -rf ffmpeg.tar.xz ffmpeg - name: Download yt-dlp binary shell: bash run: | curl -fL --retry 3 --retry-delay 2 --retry-connrefused "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp_asset }}" -o "resources/${{ matrix.ytdlp_output }}" if [[ "${{ matrix.platform }}" == "linux" ]] || [[ "${{ matrix.platform }}" == "macos" ]]; then chmod +x "resources/${{ matrix.ytdlp_output }}" fi - name: Lint and format check run: pnpm run check && pnpm run typecheck - name: Setup macOS signing if: matrix.platform == 'macos' shell: bash env: MAC_CERT_P12_BASE64: ${{ secrets.MAC_CERT_P12_BASE64 }} MAC_CERT_P12_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }} APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} run: | set -euo pipefail echo "SIGNING_AVAILABLE=false" >> "$GITHUB_ENV" # Check if all required secrets are present if [[ -z "$MAC_CERT_P12_BASE64" ]] || [[ -z "$MAC_CERT_P12_PASSWORD" ]] || \ [[ -z "$APPLE_API_KEY_ID" ]] || [[ -z "$APPLE_API_ISSUER" ]] || \ [[ -z "$APPLE_API_KEY_P8_BASE64" ]]; then echo "::notice::macOS signing secrets not available, skipping code signing setup" exit 0 fi CERT_PATH="$RUNNER_TEMP/mac_cert.p12" KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain" API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8" echo "$MAC_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH" echo "$APPLE_API_KEY_P8_BASE64" | base64 --decode > "$API_KEY_PATH" security create-keychain -p "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH" security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MAC_CERT_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild security list-keychain -d user -s "$KEYCHAIN_PATH" security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH" echo "CSC_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV" echo "CSC_KEY_PASSWORD=$MAC_CERT_P12_PASSWORD" >> "$GITHUB_ENV" echo "APPLE_API_KEY=$API_KEY_PATH" >> "$GITHUB_ENV" echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV" echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> "$GITHUB_ENV" echo "SIGNING_AVAILABLE=true" >> "$GITHUB_ENV" - name: Build application run: ${{ matrix.build_script }} - name: Verify macOS codesign and notarization if: matrix.platform == 'macos' && env.SIGNING_AVAILABLE == 'true' shell: bash run: | set -euo pipefail apps_found=0 while IFS= read -r app; do apps_found=1 echo "Verifying codesign for $app" codesign --verify --deep --strict --verbose=2 "$app" spctl -a -t exec -vv "$app" echo "Validating notarization ticket for $app" xcrun stapler validate "$app" done < <(find dist -type d -name "*.app" -prune -print) if [[ "$apps_found" -eq 0 ]]; then echo "::error::No .app bundles found in dist" exit 1 fi dmgs_found=0 while IFS= read -r dmg; do dmgs_found=1 echo "Submitting DMG for notarization: $dmg" xcrun notarytool submit "$dmg" --key "$APPLE_API_KEY" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER" --wait echo "Stapling notarization ticket for $dmg" xcrun stapler staple "$dmg" echo "Validating notarization ticket for $dmg" xcrun stapler validate "$dmg" done < <(find dist -type f -name "*.dmg" -print) if [[ "$dmgs_found" -eq 0 ]]; then echo "::notice::No DMG artifacts found to validate" fi - name: Upload build artifacts if: inputs.upload_artifacts == true uses: actions/upload-artifact@v4 with: name: dist-${{ matrix.os }} path: | dist/*.exe dist/*.zip dist/*.dmg dist/*.AppImage dist/*.snap dist/*.deb dist/*.rpm dist/*.tar.gz dist/*.yml dist/*.blockmap retention-days: 1