fix(installer): 修复卸载确认后的流程退出 #1010
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| - '*.*.*' | |
| permissions: | |
| contents: write | |
| jobs: | |
| validate_release_tag: | |
| if: >- | |
| ${{ | |
| github.event_name == 'workflow_dispatch' || | |
| startsWith(github.ref, 'refs/tags/') || | |
| ( | |
| github.ref == 'refs/heads/main' && | |
| startsWith(github.event.head_commit.message, 'chore(release):') | |
| ) | |
| }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Validate release tag matches package version | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${GITHUB_REF}" != refs/tags/* ]]; then | |
| echo "No tag push; release tag validation is not required for this run." | |
| exit 0 | |
| fi | |
| package_version="$(node -p "require('./package.json').version")" | |
| if [[ "$TAG_NAME" != "$package_version" ]]; then | |
| echo "::error::Release tag '$TAG_NAME' must exactly match package.json version '$package_version'." | |
| exit 1 | |
| fi | |
| echo "Validated release tag: $TAG_NAME" | |
| typecheck: | |
| needs: [validate_release_tag] | |
| if: >- | |
| ${{ | |
| github.event_name == 'workflow_dispatch' || | |
| startsWith(github.ref, 'refs/tags/') || | |
| ( | |
| github.ref == 'refs/heads/main' && | |
| startsWith(github.event.head_commit.message, 'chore(release):') | |
| ) | |
| }} | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup PNPM | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9.15.9 | |
| run_install: false | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile=false --prefer-offline --ignore-scripts | |
| - name: Typecheck (vue-tsc) | |
| run: npx vue-tsc --noEmit | |
| build_win: | |
| needs: [validate_release_tag] | |
| if: >- | |
| ${{ | |
| github.event_name == 'workflow_dispatch' || | |
| startsWith(github.ref, 'refs/tags/') || | |
| ( | |
| github.ref == 'refs/heads/main' && | |
| startsWith(github.event.head_commit.message, 'chore(release):') | |
| ) | |
| }} | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup PNPM | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9.15.9 | |
| run_install: false | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Restore Demucs models cache (Windows) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| vendor/demucs/models | |
| key: demucs-models-${{ runner.os }}-${{ hashFiles('scripts/demucs-model-manifest.json', 'scripts/ensure-demucs-runtime.mjs', 'scripts/lib/*.mjs') }} | |
| restore-keys: | | |
| demucs-models-${{ runner.os }}- | |
| - name: Restore Rekordbox runtime cache (Windows) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| vendor/rekordbox-desktop-runtime/win32-x64/python | |
| key: rekordbox-runtime-${{ runner.os }}-${{ hashFiles('scripts/ensure-rekordbox-desktop-runtime.mjs', 'scripts/rekordbox-desktop-runtime-requirements.txt', 'scripts/lib/demucs-standalone-python.mjs', 'scripts/lib/demucs-runtime-support.mjs') }} | |
| restore-keys: | | |
| rekordbox-runtime-${{ runner.os }}- | |
| - name: Setup Python (for node-gyp) | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Ensure setuptools available | |
| run: | | |
| python -m pip install --upgrade pip setuptools wheel | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: x86_64-pc-windows-msvc | |
| - name: Restore Cargo cache (Windows) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| rust_package/target | |
| key: cargo-${{ runner.os }}-x86_64-pc-windows-msvc-${{ hashFiles('rust_package/Cargo.lock', 'rust_package/Cargo.toml', 'rust_package/build.rs', 'rust_package/src/**', 'rust_package/native/qm/**', 'rust_package/native/soundtouch/**', 'rust_package/native/chromaprint/**', 'rust_package/native/ffmpeg/frkb_ffmpeg_decode_wrapper.*') }} | |
| restore-keys: | | |
| cargo-${{ runner.os }}-x86_64-pc-windows-msvc- | |
| - name: Restore Electron builder cache (Windows) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/AppData/Local/electron/Cache | |
| ~/AppData/Local/electron-builder/Cache | |
| key: electron-builder-${{ runner.os }}-${{ hashFiles('package.json', 'pnpm-lock.yaml') }} | |
| restore-keys: | | |
| electron-builder-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile=false --prefer-offline | |
| - name: Ensure Demucs models (Windows) | |
| run: node scripts/ensure-demucs-runtime.mjs --models-only --ci | |
| - name: Ensure Rekordbox Desktop runtime (Windows) | |
| run: pnpm run rekordbox:runtime:ensure | |
| - name: Validate Rekordbox Desktop runtime (Windows) | |
| run: pnpm run rekordbox:runtime:validate -- --platform-key win32-x64 | |
| - name: Prepare FFmpeg (Windows) | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| New-Item -ItemType Directory -Force -Path vendor/ffmpeg/win32-x64 | Out-Null | |
| $headers = @{ | |
| 'User-Agent' = 'TrackStudio-Release' | |
| 'Authorization' = "Bearer $env:GH_TOKEN" | |
| 'X-GitHub-Api-Version' = '2022-11-28' | |
| } | |
| $release = Invoke-RestMethod -Uri 'https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest' -Headers $headers | |
| $asset = | |
| $release.assets | | |
| Where-Object { $_.name -match '^ffmpeg-N-.*-win64-gpl\.zip$' } | | |
| Select-Object -First 1 | |
| if (-not $asset) { | |
| $asset = | |
| $release.assets | | |
| Where-Object { $_.name -match '^ffmpeg-.*-win64-gpl\.zip$' } | | |
| Select-Object -First 1 | |
| } | |
| if (-not $asset) { | |
| throw 'win64 gpl ffmpeg asset not found in latest BtbN release.' | |
| } | |
| Invoke-WebRequest -Uri $asset.browser_download_url -OutFile ffmpeg.zip | |
| Expand-Archive -Path ffmpeg.zip -DestinationPath ffmpeg_unzip -Force | |
| $ff = (Get-ChildItem -Recurse -Filter ffmpeg.exe ffmpeg_unzip | Select-Object -First 1).FullName | |
| $ffprobe = (Get-ChildItem -Recurse -Filter ffprobe.exe ffmpeg_unzip | Select-Object -First 1).FullName | |
| if (-not $ff) { | |
| throw "ffmpeg.exe not found in downloaded archive." | |
| } | |
| if (-not $ffprobe) { | |
| throw "ffprobe.exe not found in downloaded archive." | |
| } | |
| Copy-Item -Path $ff -Destination vendor/ffmpeg/win32-x64/ffmpeg.exe -Force | |
| Copy-Item -Path $ffprobe -Destination vendor/ffmpeg/win32-x64/ffprobe.exe -Force | |
| Remove-Item ffmpeg.zip -Force | |
| Remove-Item ffmpeg_unzip -Recurse -Force | |
| - name: Prepare FFmpeg native dependencies (Windows) | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| $headers = @{ | |
| 'User-Agent' = 'TrackStudio-Release' | |
| 'Authorization' = "Bearer $env:GH_TOKEN" | |
| 'X-GitHub-Api-Version' = '2022-11-28' | |
| } | |
| $release = Invoke-RestMethod -Uri 'https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest' -Headers $headers | |
| $asset = | |
| $release.assets | | |
| Where-Object { $_.name -match '^ffmpeg-N-.*-win64-gpl-shared.*\.zip$' } | | |
| Select-Object -First 1 | |
| if (-not $asset) { | |
| $asset = | |
| $release.assets | | |
| Where-Object { $_.name -match '^ffmpeg-.*-win64-gpl-shared.*\.zip$' } | | |
| Select-Object -First 1 | |
| } | |
| if (-not $asset) { | |
| throw 'win64 gpl shared ffmpeg asset not found in latest BtbN release.' | |
| } | |
| Invoke-WebRequest -Uri $asset.browser_download_url -OutFile ffmpeg-shared.zip | |
| Expand-Archive -Path ffmpeg-shared.zip -DestinationPath ffmpeg_shared_unzip -Force | |
| $binDir = Get-ChildItem -Recurse -Directory ffmpeg_shared_unzip | Where-Object { $_.Name -eq 'bin' } | Select-Object -First 1 | |
| if (-not $binDir) { | |
| throw "bin directory not found in shared ffmpeg archive." | |
| } | |
| $sharedRoot = $binDir.Parent.FullName | |
| $includeDir = Join-Path $sharedRoot 'include' | |
| $libDir = Join-Path $sharedRoot 'lib' | |
| if (-not (Test-Path $includeDir)) { | |
| throw "include directory not found in shared ffmpeg archive." | |
| } | |
| if (-not (Test-Path $libDir)) { | |
| throw "lib directory not found in shared ffmpeg archive." | |
| } | |
| New-Item -ItemType Directory -Force -Path vendor/ffmpeg/win32-x64/dll | Out-Null | |
| Get-ChildItem -Path $binDir.FullName -Filter "*.dll" | ForEach-Object { | |
| Copy-Item -Path $_.FullName -Destination "vendor/ffmpeg/win32-x64/dll/$($_.Name)" -Force | |
| } | |
| $nativeFfmpegDir = 'rust_package/native/ffmpeg/win32-x64' | |
| New-Item -ItemType Directory -Force -Path "$nativeFfmpegDir/include" | Out-Null | |
| New-Item -ItemType Directory -Force -Path "$nativeFfmpegDir/lib" | Out-Null | |
| foreach ($dirName in @('libavcodec', 'libavformat', 'libavutil', 'libswresample')) { | |
| Copy-Item -Path (Join-Path $includeDir $dirName) -Destination "$nativeFfmpegDir/include/" -Recurse -Force | |
| } | |
| foreach ($libName in @('avcodec.lib', 'avformat.lib', 'avutil.lib', 'swresample.lib')) { | |
| Copy-Item -Path (Join-Path $libDir $libName) -Destination "$nativeFfmpegDir/lib/$libName" -Force | |
| } | |
| Remove-Item ffmpeg-shared.zip -Force | |
| Remove-Item ffmpeg_shared_unzip -Recurse -Force | |
| Write-Host "FFmpeg DLLs copied to vendor/ffmpeg/win32-x64/dll/" | |
| Get-ChildItem vendor/ffmpeg/win32-x64/dll/ | ForEach-Object { Write-Host " $($_.Name) ($([math]::Round($_.Length/1MB,1))MB)" } | |
| Write-Host "FFmpeg native headers/libs copied to $nativeFfmpegDir" | |
| - name: Validate bundled media tools (Windows) | |
| run: node scripts/validate-bundled-media-tools.mjs --mode package --platform-key win32-x64 | |
| - name: Build Rust N-API (rust_package) | |
| working-directory: rust_package | |
| run: npx --yes @napi-rs/cli@2.18.4 build --platform --release --target x86_64-pc-windows-msvc | |
| - name: Build (vite) | |
| env: | |
| CLOUD_SYNC_BASE_URL_PROD: ${{ secrets.CLOUD_SYNC_BASE_URL_PROD }} | |
| CLOUD_SYNC_API_SECRET_KEY: ${{ secrets.CLOUD_SYNC_API_SECRET_KEY }} | |
| FRKB_LASTFM_API_KEY: ${{ secrets.FRKB_LASTFM_API_KEY }} | |
| run: pnpm build | |
| - name: Build Windows installer frontend | |
| run: pnpm installer-ui:build | |
| - name: Package (electron-builder, no publish) | |
| run: npx electron-builder --win --x64 --publish never | |
| - name: Verify packaged native module (Windows) | |
| shell: pwsh | |
| run: | | |
| $resourcesRoot = "dist/win-unpacked/resources" | |
| $dllDir = Join-Path $resourcesRoot "ffmpeg/win32-x64/dll" | |
| $nativeModule = "dist/win-unpacked/resources/app.asar.unpacked/node_modules/rust_package/index.win32-x64-msvc.node" | |
| if (-not (Test-Path $dllDir)) { | |
| throw "Packaged FFmpeg DLL directory not found: $dllDir" | |
| } | |
| if (-not (Test-Path $nativeModule)) { | |
| throw "Packaged Rust native module not found: $nativeModule" | |
| } | |
| $env:PATH = "$dllDir;$env:PATH" | |
| node -e "require('./dist/win-unpacked/resources/app.asar.unpacked/node_modules/rust_package'); console.log('Packaged Rust native module loaded.')" | |
| - name: Verify packaged Demucs resources (Windows) | |
| shell: pwsh | |
| run: | | |
| $resourcesRoot = "dist/win-unpacked/resources" | |
| if (-not (Test-Path $resourcesRoot)) { | |
| throw "Windows unpacked resources directory not found: $resourcesRoot" | |
| } | |
| $required = @( | |
| "ffmpeg/win32-x64/ffmpeg.exe", | |
| "ffmpeg/win32-x64/ffprobe.exe", | |
| "demucs/bootstrap/mixtape_demucs_bootstrap.py", | |
| "demucs/bootstrap/beat_this_bridge.py", | |
| "demucs/bootstrap/rkb_official_phase_selector.py", | |
| "demucs/models/htdemucs.yaml", | |
| "demucs/models/955717e8-8726e21a.th", | |
| "rekordboxDesktopLibrary/bridge.py", | |
| "rekordboxDesktopRuntime/win32-x64/python/python.exe", | |
| "rekordboxDesktopRuntime/win32-x64/python/Lib/site-packages/pyrekordbox/__init__.py" | |
| ) | |
| $missing = @() | |
| foreach ($relativePath in $required) { | |
| $targetPath = Join-Path $resourcesRoot $relativePath | |
| if (-not (Test-Path $targetPath)) { | |
| $missing += $relativePath | |
| } | |
| } | |
| $unexpectedRuntimeRoot = Join-Path $resourcesRoot "demucs/win32-x64" | |
| if (Test-Path $unexpectedRuntimeRoot) { | |
| $missing += "unexpected:demucs/win32-x64" | |
| } | |
| if ($missing.Count -gt 0) { | |
| Write-Error "Missing packaged Demucs resources (Windows):" | |
| $missing | ForEach-Object { Write-Error " - $_" } | |
| exit 1 | |
| } | |
| Write-Host "Packaged Demucs resources verified (Windows, runtime excluded)." | |
| python scripts/verify_packaged_python_import_closure.py ` | |
| --source-root scripts ` | |
| --packaged-root "$resourcesRoot/demucs/bootstrap" ` | |
| --entry beat_this_bridge.py | |
| # 不再复制 rc.yml,严格遵循 GitHub provider 读取 latest.yml / latest-mac.yml | |
| - name: Upload dist artifacts (Windows) | |
| uses: actions/upload-artifact@v7 | |
| if: always() && startsWith(github.ref, 'refs/tags/') | |
| with: | |
| name: win-artifacts | |
| path: | | |
| dist/*.exe | |
| dist/*.blockmap | |
| dist/*.yml | |
| build_rekordbox_runtime_mac_x64: | |
| needs: [validate_release_tag] | |
| if: >- | |
| ${{ | |
| github.event_name == 'workflow_dispatch' || | |
| startsWith(github.ref, 'refs/tags/') || | |
| ( | |
| github.ref == 'refs/heads/main' && | |
| startsWith(github.event.head_commit.message, 'chore(release):') | |
| ) | |
| }} | |
| runs-on: macos-15-intel | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Restore Rekordbox runtime cache (macOS x64) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| vendor/rekordbox-desktop-runtime/darwin-x64/python | |
| key: rekordbox-runtime-${{ runner.os }}-${{ runner.arch }}-darwin-x64-${{ hashFiles('scripts/ensure-rekordbox-desktop-runtime.mjs', 'scripts/rekordbox-desktop-runtime-requirements.txt', 'scripts/lib/demucs-standalone-python.mjs', 'scripts/lib/demucs-runtime-support.mjs') }} | |
| restore-keys: | | |
| rekordbox-runtime-${{ runner.os }}-${{ runner.arch }}-darwin-x64- | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Ensure Rekordbox Desktop runtime (macOS x64) | |
| run: node scripts/ensure-rekordbox-desktop-runtime.mjs --platform-key darwin-x64 | |
| - name: Validate Rekordbox Desktop runtime (macOS x64) | |
| run: node scripts/validate-rekordbox-desktop-runtime.mjs --platform-key darwin-x64 | |
| - name: Archive Rekordbox Desktop runtime (macOS x64) | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| tar -czf dist/rekordbox-runtime-darwin-x64.tar.gz \ | |
| -C vendor/rekordbox-desktop-runtime/darwin-x64 \ | |
| python | |
| - name: Upload Rekordbox runtime artifact (macOS x64) | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: rekordbox-runtime-darwin-x64 | |
| path: dist/rekordbox-runtime-darwin-x64.tar.gz | |
| - name: Fetch Intel FFmpeg static libs | |
| run: | | |
| set -euo pipefail | |
| # The hosted Intel runner ships an unrelated HashiCorp tap with a | |
| # broken vagrant formula, which makes `brew update` emit an error. | |
| if brew tap | grep -Fxq 'hashicorp/tap'; then | |
| # The runner may have packer installed from this tap; force only | |
| # removes the tap metadata and leaves unrelated formulae intact. | |
| brew untap --force hashicorp/tap | |
| fi | |
| brew update | |
| brew install ffmpeg | |
| mkdir -p dist/ffmpeg-x64-dev/lib dist/ffmpeg-x64-dev/include | |
| for lib in libavcodec libavformat libavutil libswresample; do | |
| cp -L "$(brew --prefix)/lib/${lib}.a" dist/ffmpeg-x64-dev/lib/ | |
| cp -RL "$(brew --prefix)/include/${lib}" dist/ffmpeg-x64-dev/include/ | |
| done | |
| - name: Upload Intel FFmpeg dev artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ffmpeg-x64-dev | |
| path: dist/ffmpeg-x64-dev | |
| build_mac: | |
| if: >- | |
| ${{ | |
| github.event_name == 'workflow_dispatch' || | |
| startsWith(github.ref, 'refs/tags/') || | |
| ( | |
| github.ref == 'refs/heads/main' && | |
| startsWith(github.event.head_commit.message, 'chore(release):') | |
| ) | |
| }} | |
| runs-on: macos-latest | |
| needs: [validate_release_tag, build_rekordbox_runtime_mac_x64] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup PNPM | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9.15.9 | |
| run_install: false | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Restore Rekordbox runtime cache (macOS arm64) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| vendor/rekordbox-desktop-runtime/darwin-arm64/python | |
| key: rekordbox-runtime-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('scripts/ensure-rekordbox-desktop-runtime.mjs', 'scripts/rekordbox-desktop-runtime-requirements.txt', 'scripts/lib/demucs-standalone-python.mjs', 'scripts/lib/demucs-runtime-support.mjs') }} | |
| restore-keys: | | |
| rekordbox-runtime-${{ runner.os }}-${{ runner.arch }}- | |
| - name: Setup Python (for node-gyp) | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Ensure setuptools available | |
| run: | | |
| python -m pip install --upgrade pip setuptools wheel | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin, x86_64-apple-darwin | |
| - name: Ensure Rust targets | |
| run: rustup target add aarch64-apple-darwin x86_64-apple-darwin | |
| - name: Restore Cargo cache (macOS) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| rust_package/target | |
| key: cargo-${{ runner.os }}-universal-${{ hashFiles('rust_package/Cargo.lock', 'rust_package/Cargo.toml', 'rust_package/build.rs', 'rust_package/src/**', 'rust_package/native/qm/**', 'rust_package/native/soundtouch/**', 'rust_package/native/chromaprint/**', 'rust_package/native/ffmpeg/frkb_ffmpeg_decode_wrapper.*') }} | |
| restore-keys: | | |
| cargo-${{ runner.os }}-universal- | |
| - name: Restore Electron builder cache (macOS) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/Library/Caches/electron | |
| ~/Library/Caches/electron-builder | |
| key: electron-builder-${{ runner.os }}-${{ hashFiles('package.json', 'pnpm-lock.yaml') }} | |
| restore-keys: | | |
| electron-builder-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile=false --prefer-offline | |
| - name: Ensure Demucs models (macOS) | |
| run: node scripts/ensure-demucs-runtime.mjs --models-only --ci | |
| - name: Ensure Rekordbox Desktop runtime (macOS arm64) | |
| run: pnpm run rekordbox:runtime:ensure | |
| - name: Download Intel FFmpeg dev artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: ffmpeg-x64-dev | |
| path: artifacts/ffmpeg-x64-dev | |
| - name: Download Rekordbox runtime artifact (macOS x64) | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: rekordbox-runtime-darwin-x64 | |
| path: artifacts/rekordbox-runtime-darwin-x64 | |
| - name: Restore Rekordbox runtime artifact (macOS x64) | |
| run: | | |
| set -euo pipefail | |
| mkdir -p vendor/rekordbox-desktop-runtime/darwin-x64 | |
| tar -xzf artifacts/rekordbox-runtime-darwin-x64/rekordbox-runtime-darwin-x64.tar.gz \ | |
| -C vendor/rekordbox-desktop-runtime/darwin-x64 | |
| - name: Validate Rekordbox Desktop runtime (macOS universal) | |
| run: pnpm run rekordbox:runtime:validate:mac-universal | |
| - name: Prepare FFmpeg (macOS both arch) | |
| run: | | |
| set -euxo pipefail | |
| mkdir -p vendor/ffmpeg/darwin-arm64 vendor/ffmpeg/darwin-x64 vendor/ffmpeg/darwin | |
| echo "=== Host uname ===" | |
| uname -a | |
| sw_vers | |
| # 使用 Homebrew 安装 arm64 版本 (native on arm64 runner) | |
| echo "Installing arm64 ffmpeg via Homebrew" | |
| # Keep unrelated broken taps from making Homebrew metadata updates noisy. | |
| if brew tap | grep -Fxq 'hashicorp/tap'; then | |
| # The runner may have packer installed from this tap; force only | |
| # removes the tap metadata and leaves unrelated formulae intact. | |
| brew untap --force hashicorp/tap | |
| fi | |
| brew update | |
| brew install ffmpeg | |
| ARM64_FFMPEG_PATH="$(brew --prefix)/bin/ffmpeg" | |
| ARM64_FFPROBE_PATH="$(brew --prefix)/bin/ffprobe" | |
| file "$ARM64_FFMPEG_PATH" | |
| file "$ARM64_FFPROBE_PATH" | |
| install -m 755 "$ARM64_FFMPEG_PATH" vendor/ffmpeg/darwin-arm64/ffmpeg | |
| install -m 755 "$ARM64_FFPROBE_PATH" vendor/ffmpeg/darwin-arm64/ffprobe | |
| download_and_verify_zip() { | |
| local url="$1" | |
| local output="$2" | |
| local attempt | |
| for attempt in 1 2 3 4 5; do | |
| rm -f "$output" | |
| echo "Downloading $output (attempt ${attempt}/5)" | |
| if curl --fail --location --retry 3 --retry-all-errors --retry-delay 2 --connect-timeout 30 --max-time 600 "$url" -o "$output" \ | |
| && unzip -tq "$output" >/dev/null; then | |
| return 0 | |
| fi | |
| echo "WARN: downloaded archive is missing or invalid: $output" | |
| if [ -f "$output" ]; then | |
| file "$output" || true | |
| wc -c "$output" || true | |
| fi | |
| sleep $((attempt * 3)) | |
| done | |
| echo "ERROR: failed to download a valid zip archive: $output" | |
| exit 1 | |
| } | |
| # 带 zip 校验重试下载 x64 版本,避免 GitHub 资产链路偶发返回错误页却被当成包解压。 | |
| echo "Downloading x64 ffmpeg/ffprobe from ffbinaries..." | |
| download_and_verify_zip "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.4.1/ffmpeg-4.4.1-osx-64.zip" ffmpeg-x64.zip | |
| download_and_verify_zip "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.4.1/ffprobe-4.4.1-osx-64.zip" ffprobe-x64.zip | |
| unzip -q ffmpeg-x64.zip -d ffmpeg-x64-temp | |
| unzip -q ffprobe-x64.zip -d ffprobe-x64-temp | |
| X64_FFMPEG_PATH="$(find ffmpeg-x64-temp -type f -name ffmpeg | head -n 1 || true)" | |
| X64_FFPROBE_PATH="$(find ffprobe-x64-temp -type f -name ffprobe | head -n 1 || true)" | |
| if [ -z "$X64_FFMPEG_PATH" ] || [ -z "$X64_FFPROBE_PATH" ]; then | |
| echo "ERROR: x64 ffmpeg/ffprobe binary missing from downloaded archives" | |
| exit 1 | |
| fi | |
| file "$X64_FFMPEG_PATH" | |
| file "$X64_FFPROBE_PATH" | |
| if ! file "$X64_FFMPEG_PATH" | grep -q "x86_64"; then | |
| echo "ERROR: x64 ffmpeg artifact is not x86_64" | |
| exit 1 | |
| fi | |
| if ! file "$X64_FFPROBE_PATH" | grep -q "x86_64"; then | |
| echo "ERROR: x64 ffprobe artifact is not x86_64" | |
| exit 1 | |
| fi | |
| install -m 755 "$X64_FFMPEG_PATH" vendor/ffmpeg/darwin-x64/ffmpeg | |
| install -m 755 "$X64_FFPROBE_PATH" vendor/ffmpeg/darwin-x64/ffprobe | |
| rm -rf ffmpeg-x64-temp ffprobe-x64-temp ffmpeg-x64.zip ffprobe-x64.zip | |
| if [ ! -f vendor/ffmpeg/darwin-x64/ffprobe ]; then | |
| echo "ERROR: x64 ffprobe is missing" | |
| exit 1 | |
| fi | |
| # 输出原始二进制信息 | |
| echo "=== Raw arm64 binary ===" | |
| file vendor/ffmpeg/darwin-arm64/ffmpeg | |
| file vendor/ffmpeg/darwin-arm64/ffprobe | |
| ls -lh vendor/ffmpeg/darwin-arm64/ffmpeg | |
| ls -lh vendor/ffmpeg/darwin-arm64/ffprobe | |
| shasum -a 256 vendor/ffmpeg/darwin-arm64/ffmpeg || true | |
| shasum -a 256 vendor/ffmpeg/darwin-arm64/ffprobe || true | |
| lipo -archs vendor/ffmpeg/darwin-arm64/ffmpeg || true | |
| echo "=== Raw x64 binary ===" | |
| file vendor/ffmpeg/darwin-x64/ffmpeg | |
| file vendor/ffmpeg/darwin-x64/ffprobe | |
| ls -lh vendor/ffmpeg/darwin-x64/ffmpeg | |
| ls -lh vendor/ffmpeg/darwin-x64/ffprobe | |
| shasum -a 256 vendor/ffmpeg/darwin-x64/ffmpeg || true | |
| shasum -a 256 vendor/ffmpeg/darwin-x64/ffprobe || true | |
| lipo -archs vendor/ffmpeg/darwin-x64/ffmpeg || true | |
| # 生成通用二进制并放入固定目录 | |
| echo "Creating universal ffmpeg via lipo" | |
| lipo -create -output vendor/ffmpeg/darwin/ffmpeg \ | |
| vendor/ffmpeg/darwin-arm64/ffmpeg \ | |
| vendor/ffmpeg/darwin-x64/ffmpeg | |
| echo "Creating universal ffprobe via lipo" | |
| lipo -create -output vendor/ffmpeg/darwin/ffprobe \ | |
| vendor/ffmpeg/darwin-arm64/ffprobe \ | |
| vendor/ffmpeg/darwin-x64/ffprobe | |
| chmod +x vendor/ffmpeg/darwin/ffmpeg | |
| chmod +x vendor/ffmpeg/darwin/ffprobe | |
| echo "=== Universal binary ===" | |
| file vendor/ffmpeg/darwin/ffmpeg | |
| file vendor/ffmpeg/darwin/ffprobe | |
| ls -lh vendor/ffmpeg/darwin/ffmpeg | |
| ls -lh vendor/ffmpeg/darwin/ffprobe | |
| shasum -a 256 vendor/ffmpeg/darwin/ffmpeg || true | |
| shasum -a 256 vendor/ffmpeg/darwin/ffprobe || true | |
| lipo -archs vendor/ffmpeg/darwin/ffmpeg || true | |
| lipo -archs vendor/ffmpeg/darwin/ffprobe || true | |
| # 清理单架构副本 | |
| rm -rf vendor/ffmpeg/darwin-arm64 vendor/ffmpeg/darwin-x64 | |
| echo "Listing vendor/ffmpeg directory" | |
| find vendor/ffmpeg -maxdepth 2 -type f -exec ls -l {} \; | |
| - name: Prepare FFmpeg native dependencies (macOS) | |
| run: | | |
| set -euxo pipefail | |
| copy_ffmpeg_dev_files() { | |
| local source_prefix="$1" | |
| local dest_dir="$2" | |
| local lib_dir="$source_prefix/lib" | |
| local include_dir="$source_prefix/include" | |
| mkdir -p "$dest_dir/lib" "$dest_dir/include" | |
| for lib in libavcodec libavformat libavutil libswresample; do | |
| if [ -f "$lib_dir/${lib}.a" ]; then | |
| cp -L "$lib_dir/${lib}.a" "$dest_dir/lib/" | |
| echo "Copied ${lib}.a from ${lib_dir}" | |
| else | |
| echo "ERROR: ${lib}.a not found at ${lib_dir}/" | |
| exit 1 | |
| fi | |
| done | |
| for dir in libavcodec libavformat libavutil libswresample; do | |
| if [ -d "$include_dir/$dir" ]; then | |
| rm -rf "$dest_dir/include/$dir" | |
| cp -RL "$include_dir/$dir" "$dest_dir/include/" | |
| echo "Copied ${dir} headers from ${include_dir}" | |
| else | |
| echo "ERROR: ${dir} headers not found at ${include_dir}/" | |
| exit 1 | |
| fi | |
| done | |
| if [ ! -f "$dest_dir/include/libavcodec/avcodec.h" ]; then | |
| echo "ERROR: copied FFmpeg headers are incomplete at ${dest_dir}/include" | |
| find "$dest_dir/include" -maxdepth 2 -type l -o -type f | head -n 40 || true | |
| exit 1 | |
| fi | |
| echo "=== FFmpeg native deps: ${dest_dir} ===" | |
| file "$dest_dir/lib/libavcodec.a" || true | |
| ls -lh "$dest_dir/lib/" | |
| ls "$dest_dir/include/" | |
| } | |
| ARM64_BREW_PREFIX="$(brew --prefix)" | |
| copy_ffmpeg_dev_files "$ARM64_BREW_PREFIX" "rust_package/native/ffmpeg/darwin-arm64" | |
| copy_ffmpeg_dev_files "artifacts/ffmpeg-x64-dev" "rust_package/native/ffmpeg/darwin-x64" | |
| if ! lipo -archs rust_package/native/ffmpeg/darwin-x64/lib/libavcodec.a | grep -q "x86_64"; then | |
| echo "ERROR: Intel FFmpeg static lib is not x86_64" | |
| lipo -archs rust_package/native/ffmpeg/darwin-x64/lib/libavcodec.a || true | |
| exit 1 | |
| fi | |
| if ! lipo -archs rust_package/native/ffmpeg/darwin-arm64/lib/libavcodec.a | grep -q "arm64"; then | |
| echo "ERROR: arm64 FFmpeg static lib is not arm64" | |
| lipo -archs rust_package/native/ffmpeg/darwin-arm64/lib/libavcodec.a || true | |
| exit 1 | |
| fi | |
| - name: Validate bundled media tools (macOS) | |
| run: node scripts/validate-bundled-media-tools.mjs --mode package --platform-key darwin-universal | |
| - name: Build Rust N-API for mac (both arch) | |
| working-directory: rust_package | |
| run: | | |
| npx --yes @napi-rs/cli@2.18.4 build --platform --release --target x86_64-apple-darwin | |
| npx --yes @napi-rs/cli@2.18.4 build --platform --release --target aarch64-apple-darwin | |
| - name: Clean FFmpeg native build inputs (macOS) | |
| run: rm -rf rust_package/native/ffmpeg/darwin-arm64 rust_package/native/ffmpeg/darwin-x64 | |
| - name: Build (vite) | |
| env: | |
| CLOUD_SYNC_BASE_URL_PROD: ${{ secrets.CLOUD_SYNC_BASE_URL_PROD }} | |
| CLOUD_SYNC_API_SECRET_KEY: ${{ secrets.CLOUD_SYNC_API_SECRET_KEY }} | |
| FRKB_LASTFM_API_KEY: ${{ secrets.FRKB_LASTFM_API_KEY }} | |
| run: pnpm build | |
| - name: Package macOS (universal, no publish) | |
| env: | |
| DEBUG: electron-builder | |
| run: | | |
| sed -i.bak '/^electron_mirror=/d' .npmrc && rm -f .npmrc.bak | |
| npx electron-builder --mac --universal --publish never | |
| - name: Verify macOS RC signing mode | |
| run: | | |
| set -euo pipefail | |
| APP_DIR="$(find dist/mac-universal -maxdepth 1 -name '*.app' -type d | head -n 1 || true)" | |
| if [ -z "$APP_DIR" ]; then | |
| echo "macOS app bundle not found under dist/mac-universal" | |
| exit 1 | |
| fi | |
| for binary_path in \ | |
| "$APP_DIR/Contents/MacOS/FRKB" \ | |
| "$APP_DIR/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework"; do | |
| signature_info="$(codesign -dvvv "$binary_path" 2>&1)" | |
| if grep -q 'flags=.*runtime' <<<"$signature_info"; then | |
| echo "Unexpected Hardened Runtime signature: $binary_path" | |
| echo "$signature_info" | |
| exit 1 | |
| fi | |
| done | |
| - name: Verify packaged Demucs resources (macOS) | |
| run: | | |
| set -euo pipefail | |
| APP_DIR="$(find dist/mac-universal -maxdepth 1 -name '*.app' -type d | head -n 1 || true)" | |
| if [ -z "$APP_DIR" ]; then | |
| echo "macOS app bundle not found under dist/mac-universal" | |
| exit 1 | |
| fi | |
| RESOURCES_DIR="${APP_DIR}/Contents/Resources" | |
| if [ ! -d "$RESOURCES_DIR" ]; then | |
| echo "macOS resources directory not found: ${RESOURCES_DIR}" | |
| exit 1 | |
| fi | |
| REQUIRED_PATHS=( | |
| "ffmpeg/darwin/ffmpeg" | |
| "ffmpeg/darwin/ffprobe" | |
| "demucs/bootstrap/mixtape_demucs_bootstrap.py" | |
| "demucs/bootstrap/beat_this_bridge.py" | |
| "demucs/bootstrap/rkb_official_phase_selector.py" | |
| "demucs/models/htdemucs.yaml" | |
| "demucs/models/955717e8-8726e21a.th" | |
| "rekordboxDesktopLibrary/bridge.py" | |
| "rekordboxDesktopRuntime/darwin-arm64/python/bin/python3" | |
| "rekordboxDesktopRuntime/darwin-x64/python/bin/python3" | |
| ) | |
| UNEXPECTED_PATHS=( | |
| "demucs/darwin-arm64" | |
| "demucs/darwin-x64" | |
| "demucs/win32-x64" | |
| ) | |
| MISSING=0 | |
| for relative_path in "${REQUIRED_PATHS[@]}"; do | |
| target_path="${RESOURCES_DIR}/${relative_path}" | |
| if [ ! -e "$target_path" ]; then | |
| echo "Missing packaged Demucs resource (macOS): ${relative_path}" | |
| MISSING=1 | |
| fi | |
| done | |
| for relative_path in "${UNEXPECTED_PATHS[@]}"; do | |
| target_path="${RESOURCES_DIR}/${relative_path}" | |
| if [ -e "$target_path" ]; then | |
| echo "Unexpected packaged Demucs runtime (macOS): ${relative_path}" | |
| MISSING=1 | |
| fi | |
| done | |
| if [ "$MISSING" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| python3 scripts/verify_packaged_python_import_closure.py \ | |
| --source-root scripts \ | |
| --packaged-root "${RESOURCES_DIR}/demucs/bootstrap" \ | |
| --entry beat_this_bridge.py | |
| echo "Packaged Demucs resources verified (macOS, runtime excluded)." | |
| # 不再复制 rc.yml / rc-mac.yml,保持标准文件名 | |
| - name: Upload dist artifacts (macOS) | |
| uses: actions/upload-artifact@v7 | |
| if: always() && startsWith(github.ref, 'refs/tags/') | |
| with: | |
| name: mac-artifacts | |
| path: | | |
| dist/*.dmg | |
| dist/*.yml | |
| dist/Track-Studio-*-mac-universal.zip | |
| dist/Track-Studio-*-mac-universal.zip.blockmap | |
| dist/*.blockmap | |
| dist/*.zip | |
| release: | |
| if: >- | |
| ${{ | |
| (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && | |
| startsWith(github.ref, 'refs/tags/') | |
| }} | |
| needs: [validate_release_tag, typecheck, build_win, build_mac] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download Windows artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: win-artifacts | |
| path: artifacts/win | |
| - name: Download macOS artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: mac-artifacts | |
| path: artifacts/mac | |
| - name: Show downloaded files | |
| run: ls -R artifacts | cat | |
| - name: Create/Update GitHub Release and upload assets | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG_NAME: ${{ github.ref_name }} | |
| PRERELEASE: ${{ contains(github.ref_name, '-') }} | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| assets=( | |
| artifacts/win/*.exe | |
| artifacts/win/latest.yml | |
| artifacts/win/*.blockmap | |
| artifacts/mac/*.dmg | |
| artifacts/mac/latest-mac.yml | |
| artifacts/mac/*.zip | |
| artifacts/mac/*.blockmap | |
| ) | |
| if [ ${#assets[@]} -eq 0 ]; then | |
| echo "No release assets found." | |
| exit 1 | |
| fi | |
| if gh release view "$TAG_NAME" >/dev/null 2>&1; then | |
| gh release upload "$TAG_NAME" "${assets[@]}" --clobber | |
| if [ "$PRERELEASE" = "true" ]; then | |
| gh release edit "$TAG_NAME" --title "$TAG_NAME" --prerelease | |
| else | |
| gh release edit "$TAG_NAME" --title "$TAG_NAME" --latest | |
| fi | |
| else | |
| if [ "$PRERELEASE" = "true" ]; then | |
| gh release create "$TAG_NAME" "${assets[@]}" --title "$TAG_NAME" --prerelease --verify-tag --generate-notes | |
| else | |
| gh release create "$TAG_NAME" "${assets[@]}" --title "$TAG_NAME" --latest --verify-tag --generate-notes | |
| fi | |
| fi | |
| - name: Remove superseded RC releases | |
| if: contains(github.ref_name, '-rc.') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| base_version="${TAG_NAME%%-rc.*}" | |
| mapfile -t stale_tags < <( | |
| gh release list --limit 100 --json tagName,isPrerelease \ | |
| --jq ".[] | select(.isPrerelease and .tagName != \"${TAG_NAME}\" and (.tagName | startswith(\"${base_version}-rc.\"))) | .tagName" | |
| ) | |
| for stale_tag in "${stale_tags[@]}"; do | |
| echo "Removing superseded RC release and tag: ${stale_tag}" | |
| gh release delete "${stale_tag}" --yes --cleanup-tag | |
| done |