Release 1.1.4 #48
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 Desktop App | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: "v-prefixed release tag to create or update, for example v1.0.2" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release-desktop-${{ inputs.release_tag || github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| name: ${{ matrix.name }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: macOS Apple Silicon | |
| os: macos-14 | |
| platform: mac | |
| desktop-platform: darwin | |
| desktop-arch: aarch64 | |
| args: --target aarch64-apple-darwin --bundles app,dmg --config '{"bundle":{"createUpdaterArtifacts":true}}' | |
| rust-targets: aarch64-apple-darwin | |
| sidecar-target: aarch64-apple-darwin | |
| sidecar-ext: "" | |
| desktop-smoke-timeout: "30" | |
| - name: macOS Intel | |
| os: macos-15-intel | |
| platform: mac | |
| desktop-platform: darwin | |
| desktop-arch: x86_64 | |
| args: --target x86_64-apple-darwin --bundles app,dmg --config '{"bundle":{"createUpdaterArtifacts":true}}' | |
| rust-targets: x86_64-apple-darwin | |
| sidecar-target: x86_64-apple-darwin | |
| sidecar-ext: "" | |
| desktop-smoke-timeout: "90" | |
| - name: Linux | |
| os: ubuntu-22.04 | |
| platform: linux | |
| desktop-platform: linux | |
| desktop-arch: x86_64 | |
| args: --config '{"bundle":{"createUpdaterArtifacts":true}}' | |
| rust-targets: "" | |
| sidecar-target: x86_64-unknown-linux-gnu | |
| sidecar-ext: "" | |
| desktop-smoke-timeout: "30" | |
| - name: Windows | |
| os: windows-latest | |
| platform: windows | |
| desktop-platform: windows | |
| desktop-arch: x86_64 | |
| args: --config '{"bundle":{"createUpdaterArtifacts":true}}' | |
| rust-targets: "" | |
| sidecar-target: x86_64-pc-windows-msvc | |
| sidecar-ext: ".exe" | |
| desktop-smoke-timeout: "90" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "20.19.0" | |
| cache: npm | |
| cache-dependency-path: apps/package-lock.json | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| - name: Verify release version matches tag | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| python scripts/verify_release_version.py --expected "$RELEASE_TAG" --require-v-prefix | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.rust-targets }} | |
| - name: Install Linux system dependencies | |
| if: matrix.os == 'ubuntu-22.04' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf rpm cpio xvfb | |
| - name: Install app dependencies | |
| working-directory: apps | |
| run: npm ci | |
| - name: Build bundled backend sidecar | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e ".[desktop]" | |
| python scripts/build-desktop-sidecar.py --target ${{ matrix.sidecar-target }} | |
| - name: Smoke test bundled backend sidecar | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p smoke-reports | |
| report="smoke-reports/backend-sidecar-${{ matrix.sidecar-target }}.json" | |
| python scripts/smoke-test-backend-sidecar.py \ | |
| "apps/desktop/src-tauri/binaries/cc-branch-backend-${{ matrix.sidecar-target }}${{ matrix.sidecar-ext }}" \ | |
| --expected-version "${{ inputs.release_tag || github.ref_name }}" \ | |
| --expected-platform "${{ matrix.desktop-platform }}" \ | |
| --expected-arch "${{ matrix.desktop-arch }}" | tee "$report" | |
| { | |
| echo "### Backend sidecar smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$report" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Validate updater signing secrets | |
| shell: bash | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| missing=0 | |
| if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then | |
| echo "::error::Missing TAURI_SIGNING_PRIVATE_KEY secret for signed updater artifacts." | |
| missing=1 | |
| fi | |
| if [ -z "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" ]; then | |
| echo "::error::Missing TAURI_SIGNING_PRIVATE_KEY_PASSWORD secret for signed updater artifacts." | |
| missing=1 | |
| fi | |
| [ "$missing" -eq 0 ] | |
| - name: Validate macOS signing and notarization secrets | |
| if: matrix.platform == 'mac' | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE || secrets.APPLE_CERTIFICATE_BASE64 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD || secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| missing=0 | |
| if [ -z "$APPLE_CERTIFICATE" ]; then | |
| echo "::error::Missing APPLE_CERTIFICATE or APPLE_CERTIFICATE_BASE64 secret." | |
| missing=1 | |
| fi | |
| if [ -z "$APPLE_CERTIFICATE_PASSWORD" ]; then | |
| echo "::error::Missing APPLE_CERTIFICATE_PASSWORD secret." | |
| missing=1 | |
| fi | |
| if [ -z "$APPLE_SIGNING_IDENTITY" ]; then | |
| echo "APPLE_SIGNING_IDENTITY is not configured; the workflow will derive it from the imported certificate." | |
| fi | |
| has_api_key=false | |
| if [ -n "$APPLE_API_KEY_BASE64" ] && [ -n "$APPLE_API_KEY_ID" ] && [ -n "$APPLE_API_ISSUER" ]; then | |
| has_api_key=true | |
| fi | |
| has_apple_id=false | |
| if [ -n "$APPLE_ID" ] && [ -n "$APPLE_PASSWORD" ] && [ -n "$APPLE_TEAM_ID" ]; then | |
| has_apple_id=true | |
| fi | |
| if [ "$has_api_key" != "true" ] && [ "$has_apple_id" != "true" ]; then | |
| echo "::error::Missing notarization credentials. Configure APPLE_API_KEY_BASE64 + APPLE_API_KEY_ID + APPLE_API_ISSUER, or APPLE_ID + APPLE_PASSWORD/APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID." | |
| missing=1 | |
| fi | |
| [ "$missing" -eq 0 ] | |
| - name: Import Apple Developer certificate | |
| if: matrix.platform == 'mac' | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE || secrets.APPLE_CERTIFICATE_BASE64 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| certificate_path="$RUNNER_TEMP/apple-certificate.p12" | |
| keychain_password="${KEYCHAIN_PASSWORD:-$(uuidgen)}" | |
| keychain_path="$RUNNER_TEMP/build.keychain-db" | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > "$certificate_path" | |
| security create-keychain -p "$keychain_password" "$keychain_path" | |
| security set-keychain-settings -lut 21600 "$keychain_path" | |
| security unlock-keychain -p "$keychain_password" "$keychain_path" | |
| security import "$certificate_path" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path" | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path" | |
| security list-keychain -d user -s "$keychain_path" | |
| security find-identity -v -p codesigning "$keychain_path" | |
| if [ -n "$APPLE_SIGNING_IDENTITY" ]; then | |
| resolved_identity="$APPLE_SIGNING_IDENTITY" | |
| else | |
| resolved_identity="$(security find-identity -v -p codesigning "$keychain_path" | awk -F'"' '/Developer ID Application/ { print $2; exit }')" | |
| fi | |
| if [ -z "$resolved_identity" ]; then | |
| echo "::error::Could not find a Developer ID Application signing identity in the imported certificate." | |
| exit 1 | |
| fi | |
| echo "APPLE_SIGNING_IDENTITY=$resolved_identity" >> "$GITHUB_ENV" | |
| echo "Imported signing identity: $resolved_identity" | |
| - name: Prepare Apple API key for notarization | |
| if: matrix.platform == 'mac' | |
| env: | |
| APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| run: | | |
| if [ -z "$APPLE_API_KEY_BASE64" ] || [ -z "$APPLE_API_KEY_ID" ] || [ -z "$APPLE_API_ISSUER" ]; then | |
| echo "App Store Connect API key secrets are not configured; falling back to Apple ID notarization if available." | |
| exit 0 | |
| fi | |
| api_key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" | |
| echo "$APPLE_API_KEY_BASE64" | base64 --decode > "$api_key_path" | |
| chmod 600 "$api_key_path" | |
| echo "APPLE_API_KEY_PATH=$api_key_path" >> "$GITHUB_ENV" | |
| echo "APPLE_API_KEY=$APPLE_API_KEY_ID" >> "$GITHUB_ENV" | |
| echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> "$GITHUB_ENV" | |
| echo "Prepared App Store Connect API key for notarization." | |
| - name: Clean previous desktop bundle outputs | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ -d apps/desktop/src-tauri/target ]; then | |
| find apps/desktop/src-tauri/target -path '*/release/bundle' -type d -prune -exec rm -rf {} + | |
| fi | |
| - name: Build desktop app and upload release assets | |
| uses: tauri-apps/tauri-action@v0 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE || secrets.APPLE_CERTIFICATE_BASE64 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| with: | |
| projectPath: apps/desktop | |
| tagName: ${{ inputs.release_tag || github.ref_name }} | |
| releaseName: CC Branch Desktop ${{ inputs.release_tag || github.ref_name }} | |
| releaseBody: | | |
| # CC Branch Desktop | |
| This desktop build bundles the CC Branch backend sidecar. You do not need | |
| to install Python or the `cc-branch` Python package separately. | |
| ## Which file should I download? | |
| | Platform | Recommended download | Notes | | |
| | --- | --- | --- | | |
| | macOS Apple Silicon | `.dmg` asset containing `aarch64` | Signed and notarized DMG for M1/M2/M3/M4 Macs. | | |
| | macOS Intel | `.dmg` asset containing `x64` | Signed and notarized DMG for older Intel Macs. | | |
| | Windows | `.msi` asset containing `x64_en-US` | Standard Windows installer. Use the `.exe` setup file only if you prefer an executable installer. | | |
| | Ubuntu / Debian | `.deb` asset containing `amd64` | Best choice for Debian-based Linux distributions. | | |
| | Fedora / RHEL | `.rpm` asset containing `x86_64` | Best choice for RPM-based Linux distributions. | | |
| | Other Linux | `.AppImage` asset containing `amd64` | Portable Linux build. Larger because it bundles more runtime dependencies. | | |
| ## Desktop updates | |
| This release includes signed updater metadata. After installing the desktop | |
| app, open Settings to check for future updates or keep automatic checks on. | |
| ## CLI package | |
| CLI users can install the Python package from PyPI with | |
| `python3.10 -m pip install --upgrade cc-branch` or any newer | |
| supported Python. The desktop app already bundles its backend. | |
| releaseDraft: true | |
| prerelease: false | |
| includeUpdaterJson: false | |
| retryAttempts: 2 | |
| args: ${{ matrix.args }} | |
| - name: Smoke test packaged macOS desktop app | |
| if: matrix.platform == 'mac' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| app_paths_file="$(mktemp)" | |
| trap 'rm -f "$app_paths_file"' EXIT | |
| find apps/desktop/src-tauri/target -path '*/bundle/macos/CC Branch.app' -type d > "$app_paths_file" | |
| app_count="$(wc -l < "$app_paths_file" | tr -d ' ')" | |
| if [ "$app_count" -eq 0 ]; then | |
| echo "::error::Could not find generated macOS app bundle." | |
| exit 1 | |
| fi | |
| if [ "$app_count" -gt 1 ]; then | |
| echo "::error::Multiple generated macOS app bundles found." >&2 | |
| cat "$app_paths_file" >&2 | |
| exit 1 | |
| fi | |
| app_path="$(cat "$app_paths_file")" | |
| mkdir -p smoke-reports | |
| report="smoke-reports/desktop-app-${{ matrix.sidecar-target }}.json" | |
| python scripts/smoke-test-desktop-app.py "$app_path" \ | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" \ | |
| --use-auto-port \ | |
| --expected-version "${{ inputs.release_tag || github.ref_name }}" \ | |
| --expected-platform "${{ matrix.desktop-platform }}" \ | |
| --expected-arch "${{ matrix.desktop-arch }}" | tee "$report" | |
| missing_sidecar_dir="$(mktemp -d)" | |
| missing_sidecar_app="$missing_sidecar_dir/CC Branch.app" | |
| cp -R "$app_path" "$missing_sidecar_app" | |
| rm -f "$missing_sidecar_app/Contents/MacOS/cc-branch-backend" | |
| missing_sidecar_executable="$missing_sidecar_app/Contents/MacOS/cc-branch" | |
| chmod +x "$missing_sidecar_executable" | |
| failure_report="smoke-reports/desktop-app-missing-sidecar-${{ matrix.sidecar-target }}.json" | |
| python scripts/smoke-test-desktop-app.py "$missing_sidecar_executable" \ | |
| --expect-startup-failure \ | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" | tee "$failure_report" | |
| recovery_report="smoke-reports/desktop-app-recovery-${{ matrix.sidecar-target }}.json" | |
| python scripts/smoke-test-desktop-app.py "$missing_sidecar_executable" \ | |
| --expect-startup-failure-recovery \ | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" \ | |
| --recovery-sidecar "$app_path/Contents/MacOS/cc-branch-backend" \ | |
| --expected-version "${{ inputs.release_tag || github.ref_name }}" \ | |
| --expected-platform "${{ matrix.desktop-platform }}" \ | |
| --expected-arch "${{ matrix.desktop-arch }}" | tee "$recovery_report" | |
| rm -rf "$missing_sidecar_dir" | |
| stale_report="smoke-reports/desktop-app-stale-backend-${{ matrix.sidecar-target }}.json" | |
| python scripts/smoke-test-desktop-app.py "$app_path" \ | |
| --expect-stale-backend-rejection \ | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" | tee "$stale_report" | |
| { | |
| echo "### Packaged desktop smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$report" | |
| echo '```' | |
| echo "### Missing sidecar failure smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$failure_report" | |
| echo '```' | |
| echo "### Startup failure recovery smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$recovery_report" | |
| echo '```' | |
| echo "### Stale backend rejection smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$stale_report" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Smoke test packaged Linux desktop app | |
| if: matrix.platform == 'linux' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| executable="apps/desktop/src-tauri/target/release/cc-branch" | |
| if [ ! -x "$executable" ]; then | |
| echo "::error::Could not find generated Linux app executable: $executable" | |
| exit 1 | |
| fi | |
| mkdir -p smoke-reports | |
| report="smoke-reports/desktop-app-${{ matrix.sidecar-target }}.json" | |
| xvfb-run -a python scripts/smoke-test-desktop-app.py "$executable" \ | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" \ | |
| --use-auto-port \ | |
| --expected-version "${{ inputs.release_tag || github.ref_name }}" \ | |
| --expected-platform "${{ matrix.desktop-platform }}" \ | |
| --expected-arch "${{ matrix.desktop-arch }}" | tee "$report" | |
| missing_sidecar_dir="$(mktemp -d)" | |
| cp "$executable" "$missing_sidecar_dir/cc-branch" | |
| chmod +x "$missing_sidecar_dir/cc-branch" | |
| failure_report="smoke-reports/desktop-app-missing-sidecar-${{ matrix.sidecar-target }}.json" | |
| xvfb-run -a python scripts/smoke-test-desktop-app.py "$missing_sidecar_dir/cc-branch" \ | |
| --expect-startup-failure \ | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" | tee "$failure_report" | |
| recovery_report="smoke-reports/desktop-app-recovery-${{ matrix.sidecar-target }}.json" | |
| xvfb-run -a python scripts/smoke-test-desktop-app.py "$missing_sidecar_dir/cc-branch" \ | |
| --expect-startup-failure-recovery \ | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" \ | |
| --recovery-sidecar "$(dirname "$executable")/cc-branch-backend" \ | |
| --expected-version "${{ inputs.release_tag || github.ref_name }}" \ | |
| --expected-platform "${{ matrix.desktop-platform }}" \ | |
| --expected-arch "${{ matrix.desktop-arch }}" | tee "$recovery_report" | |
| rm -rf "$missing_sidecar_dir" | |
| stale_report="smoke-reports/desktop-app-stale-backend-${{ matrix.sidecar-target }}.json" | |
| xvfb-run -a python scripts/smoke-test-desktop-app.py "$executable" \ | |
| --expect-stale-backend-rejection \ | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" | tee "$stale_report" | |
| { | |
| echo "### Packaged desktop smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$report" | |
| echo '```' | |
| echo "### Missing sidecar failure smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$failure_report" | |
| echo '```' | |
| echo "### Startup failure recovery smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$recovery_report" | |
| echo '```' | |
| echo "### Stale backend rejection smoke: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$stale_report" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Smoke test packaged Windows desktop app | |
| if: matrix.platform == 'windows' | |
| shell: pwsh | |
| run: | | |
| $executable = "apps/desktop/src-tauri/target/release/cc-branch.exe" | |
| if (!(Test-Path $executable)) { | |
| Write-Error "Could not find generated Windows app executable: $executable" | |
| exit 1 | |
| } | |
| New-Item -ItemType Directory -Force -Path smoke-reports | Out-Null | |
| $report = "smoke-reports/desktop-app-${{ matrix.sidecar-target }}.json" | |
| python scripts/smoke-test-desktop-app.py $executable ` | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" ` | |
| --use-auto-port ` | |
| --expected-version "${{ inputs.release_tag || github.ref_name }}" ` | |
| --expected-platform "${{ matrix.desktop-platform }}" ` | |
| --expected-arch "${{ matrix.desktop-arch }}" | Tee-Object -FilePath $report | |
| $missingSidecarDir = Join-Path $env:RUNNER_TEMP "cc-branch-missing-sidecar" | |
| Remove-Item -Recurse -Force $missingSidecarDir -ErrorAction SilentlyContinue | |
| New-Item -ItemType Directory -Force -Path $missingSidecarDir | Out-Null | |
| $missingExecutable = Join-Path $missingSidecarDir "cc-branch.exe" | |
| Copy-Item $executable $missingExecutable -Force | |
| $failureReport = "smoke-reports/desktop-app-missing-sidecar-${{ matrix.sidecar-target }}.json" | |
| python scripts/smoke-test-desktop-app.py $missingExecutable ` | |
| --expect-startup-failure ` | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" | Tee-Object -FilePath $failureReport | |
| $recoveryReport = "smoke-reports/desktop-app-recovery-${{ matrix.sidecar-target }}.json" | |
| $recoverySidecar = "apps/desktop/src-tauri/target/release/cc-branch-backend.exe" | |
| python scripts/smoke-test-desktop-app.py $missingExecutable ` | |
| --expect-startup-failure-recovery ` | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" ` | |
| --recovery-sidecar $recoverySidecar ` | |
| --expected-version "${{ inputs.release_tag || github.ref_name }}" ` | |
| --expected-platform "${{ matrix.desktop-platform }}" ` | |
| --expected-arch "${{ matrix.desktop-arch }}" | Tee-Object -FilePath $recoveryReport | |
| $staleReport = "smoke-reports/desktop-app-stale-backend-${{ matrix.sidecar-target }}.json" | |
| python scripts/smoke-test-desktop-app.py $executable ` | |
| --expect-stale-backend-rejection ` | |
| --timeout "${{ matrix.desktop-smoke-timeout }}" | Tee-Object -FilePath $staleReport | |
| "### Packaged desktop smoke: ${{ matrix.name }}" >> $env:GITHUB_STEP_SUMMARY | |
| '```json' >> $env:GITHUB_STEP_SUMMARY | |
| Get-Content $report >> $env:GITHUB_STEP_SUMMARY | |
| '```' >> $env:GITHUB_STEP_SUMMARY | |
| "### Missing sidecar failure smoke: ${{ matrix.name }}" >> $env:GITHUB_STEP_SUMMARY | |
| '```json' >> $env:GITHUB_STEP_SUMMARY | |
| Get-Content $failureReport >> $env:GITHUB_STEP_SUMMARY | |
| '```' >> $env:GITHUB_STEP_SUMMARY | |
| "### Startup failure recovery smoke: ${{ matrix.name }}" >> $env:GITHUB_STEP_SUMMARY | |
| '```json' >> $env:GITHUB_STEP_SUMMARY | |
| Get-Content $recoveryReport >> $env:GITHUB_STEP_SUMMARY | |
| '```' >> $env:GITHUB_STEP_SUMMARY | |
| "### Stale backend rejection smoke: ${{ matrix.name }}" >> $env:GITHUB_STEP_SUMMARY | |
| '```json' >> $env:GITHUB_STEP_SUMMARY | |
| Get-Content $staleReport >> $env:GITHUB_STEP_SUMMARY | |
| '```' >> $env:GITHUB_STEP_SUMMARY | |
| - name: Verify macOS DMG contains backend sidecar | |
| if: matrix.platform == 'mac' | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| dmg_paths_file="$(mktemp)" | |
| trap 'rm -f "$dmg_paths_file"' EXIT | |
| find apps/desktop/src-tauri/target -path '*/bundle/dmg/*.dmg' -type f > "$dmg_paths_file" | |
| dmg_count="$(wc -l < "$dmg_paths_file" | tr -d ' ')" | |
| if [ "$dmg_count" -eq 0 ]; then | |
| echo "::error::Could not find generated macOS DMG." | |
| exit 1 | |
| fi | |
| if [ "$dmg_count" -gt 1 ]; then | |
| echo "::error::Multiple generated macOS DMGs found." >&2 | |
| cat "$dmg_paths_file" >&2 | |
| exit 1 | |
| fi | |
| dmg_path="$(cat "$dmg_paths_file")" | |
| mkdir -p smoke-reports | |
| report="smoke-reports/dmg-verification-${{ matrix.sidecar-target }}.json" | |
| python scripts/verify-macos-dmg.py "$dmg_path" --launch-app --verify-installed-copy --verify-stale-backend-rejection --expected-version "$version" --launch-timeout "${{ matrix.desktop-smoke-timeout }}" | tee "$report" | |
| { | |
| echo "### macOS DMG verification: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$report" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Verify Linux installers contain backend sidecar | |
| if: matrix.platform == 'linux' | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| mkdir -p smoke-reports | |
| report="smoke-reports/linux-installers-${{ matrix.sidecar-target }}.json" | |
| xvfb-run -a python scripts/verify-desktop-installers.py \ | |
| --bundle-dir apps/desktop/src-tauri/target/release/bundle \ | |
| --platform linux \ | |
| --expected-version "$version" \ | |
| --launch-timeout "${{ matrix.desktop-smoke-timeout }}" \ | |
| --launch-linux-packages --launch-appimage | tee "$report" | |
| { | |
| echo "### Linux installer verification: ${{ matrix.name }}" | |
| echo '```json' | |
| cat "$report" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Verify Windows installers contain backend sidecar | |
| if: matrix.platform == 'windows' | |
| shell: pwsh | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| $version = $env:RELEASE_TAG.TrimStart("v") | |
| New-Item -ItemType Directory -Force -Path smoke-reports | Out-Null | |
| $report = "smoke-reports/windows-installers-${{ matrix.sidecar-target }}.json" | |
| python scripts/verify-desktop-installers.py ` | |
| --bundle-dir apps/desktop/src-tauri/target/release/bundle ` | |
| --platform windows ` | |
| --expected-version $version ` | |
| --launch-timeout "${{ matrix.desktop-smoke-timeout }}" ` | |
| --launch-windows-msi ` | |
| --launch-windows-nsis | Tee-Object -FilePath $report | |
| "### Windows installer verification: ${{ matrix.name }}" >> $env:GITHUB_STEP_SUMMARY | |
| '```json' >> $env:GITHUB_STEP_SUMMARY | |
| Get-Content $report >> $env:GITHUB_STEP_SUMMARY | |
| '```' >> $env:GITHUB_STEP_SUMMARY | |
| - name: Upload desktop smoke reports | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: desktop-smoke-reports-${{ matrix.platform }}-${{ matrix.sidecar-target }} | |
| path: smoke-reports/*.json | |
| if-no-files-found: error | |
| - name: Notarize and replace macOS DMG asset | |
| if: matrix.platform == 'mac' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD || secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| dmg_paths_file="$(mktemp)" | |
| trap 'rm -f "$dmg_paths_file"' EXIT | |
| find apps/desktop/src-tauri/target -path '*/bundle/dmg/*.dmg' -type f > "$dmg_paths_file" | |
| dmg_count="$(wc -l < "$dmg_paths_file" | tr -d ' ')" | |
| if [ "$dmg_count" -eq 0 ]; then | |
| echo "::error::Could not find generated macOS DMG." | |
| exit 1 | |
| fi | |
| if [ "$dmg_count" -gt 1 ]; then | |
| echo "::error::Multiple generated macOS DMGs found." >&2 | |
| cat "$dmg_paths_file" >&2 | |
| exit 1 | |
| fi | |
| dmg_path="$(cat "$dmg_paths_file")" | |
| codesign --force --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$dmg_path" | |
| if [ -n "${APPLE_API_KEY_PATH:-}" ] && [ -n "${APPLE_API_KEY:-}" ] && [ -n "${APPLE_API_ISSUER:-}" ]; then | |
| xcrun notarytool submit "$dmg_path" \ | |
| --key "$APPLE_API_KEY_PATH" \ | |
| --key-id "$APPLE_API_KEY" \ | |
| --issuer "$APPLE_API_ISSUER" \ | |
| --wait | |
| else | |
| xcrun notarytool submit "$dmg_path" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --wait | |
| fi | |
| xcrun stapler staple "$dmg_path" | |
| xcrun stapler validate "$dmg_path" | |
| spctl -a -vv -t open --context context:primary-signature "$dmg_path" | |
| gh release upload "$RELEASE_TAG" "$dmg_path" --repo "$GITHUB_REPOSITORY" --clobber | |
| publish-updater-json: | |
| name: Publish updater metadata | |
| runs-on: ubuntu-22.04 | |
| needs: build | |
| if: ${{ needs.build.result == 'success' }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| - name: Publish complete latest.json | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| tmp="$(mktemp -d)" | |
| trap 'rm -rf "$tmp"' EXIT | |
| assets_json="$tmp/assets.json" | |
| release_notes="$tmp/release-notes.md" | |
| gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "*.sig" --dir "$tmp" | |
| gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json assets > "$assets_json" | |
| assets="$(jq -r '.assets[].name' "$assets_json")" | |
| pub_date="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" | |
| pick_asset() { | |
| local pattern="$1" | |
| local matches | |
| local match_count | |
| matches="$(printf '%s\n' "$assets" | grep -E "$pattern" || true)" | |
| match_count="$(printf '%s\n' "$matches" | sed '/^$/d' | wc -l | tr -d ' ')" | |
| if [ "$match_count" -eq 0 ]; then | |
| echo "::error::Missing release asset matching pattern: $pattern" >&2 | |
| printf '%s\n' "$assets" >&2 | |
| exit 1 | |
| fi | |
| if [ "$match_count" -gt 1 ]; then | |
| echo "::error::Multiple release assets matching pattern: $pattern" >&2 | |
| printf '%s\n' "$matches" >&2 | |
| exit 1 | |
| fi | |
| printf '%s' "$matches" | |
| } | |
| sig() { | |
| local file="$1.sig" | |
| if [ ! -f "$tmp/$file" ]; then | |
| echo "::error::Missing updater signature asset: $file" >&2 | |
| exit 1 | |
| fi | |
| tr -d '\n' < "$tmp/$file" | |
| } | |
| url_path() { | |
| python -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$1" | |
| } | |
| url() { | |
| printf 'https://github.com/%s/releases/download/%s/%s' "$GITHUB_REPOSITORY" "$RELEASE_TAG" "$(url_path "$1")" | |
| } | |
| mac_arm="$(pick_asset 'aarch64\.app\.tar\.gz$')" | |
| mac_intel="$(pick_asset 'x64\.app\.tar\.gz$')" | |
| mac_arm_dmg="$(pick_asset 'aarch64.*\.dmg$')" | |
| mac_intel_dmg="$(pick_asset 'x64.*\.dmg$')" | |
| win_msi="$(pick_asset 'x64_en-US\.msi$')" | |
| win_nsis="$(pick_asset 'x64-setup\.exe$')" | |
| linux_appimage="$(pick_asset 'amd64\.AppImage$')" | |
| linux_deb="$(pick_asset 'amd64\.deb$')" | |
| linux_rpm="$(pick_asset 'x86_64\.rpm$')" | |
| checksum_dir="$tmp/checksums" | |
| mkdir -p "$checksum_dir" | |
| checksum_assets=( | |
| "$mac_arm" | |
| "$mac_intel" | |
| "$mac_arm_dmg" | |
| "$mac_intel_dmg" | |
| "$win_msi" | |
| "$win_nsis" | |
| "$linux_appimage" | |
| "$linux_deb" | |
| "$linux_rpm" | |
| ) | |
| for asset in "${checksum_assets[@]}"; do | |
| gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "$asset" --dir "$checksum_dir" | |
| done | |
| ( | |
| cd "$checksum_dir" | |
| for asset in "${checksum_assets[@]}"; do | |
| sha256sum -- "$asset" | |
| done | |
| ) > "$tmp/SHA256SUMS" | |
| gh release upload "$RELEASE_TAG" "$tmp/SHA256SUMS" --repo "$GITHUB_REPOSITORY" --clobber | |
| python scripts/render_desktop_release_notes.py \ | |
| --tag "$RELEASE_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --assets-json "$assets_json" \ | |
| --output "$release_notes" | |
| notes="$(cat "$release_notes")" | |
| jq -n \ | |
| --arg version "$version" \ | |
| --arg notes "$notes" \ | |
| --arg pub_date "$pub_date" \ | |
| --arg mac_arm_url "$(url "$mac_arm")" \ | |
| --arg mac_arm_sig "$(sig "$mac_arm")" \ | |
| --arg mac_intel_url "$(url "$mac_intel")" \ | |
| --arg mac_intel_sig "$(sig "$mac_intel")" \ | |
| --arg win_msi_url "$(url "$win_msi")" \ | |
| --arg win_msi_sig "$(sig "$win_msi")" \ | |
| --arg win_nsis_url "$(url "$win_nsis")" \ | |
| --arg win_nsis_sig "$(sig "$win_nsis")" \ | |
| --arg linux_appimage_url "$(url "$linux_appimage")" \ | |
| --arg linux_appimage_sig "$(sig "$linux_appimage")" \ | |
| --arg linux_deb_url "$(url "$linux_deb")" \ | |
| --arg linux_deb_sig "$(sig "$linux_deb")" \ | |
| --arg linux_rpm_url "$(url "$linux_rpm")" \ | |
| --arg linux_rpm_sig "$(sig "$linux_rpm")" \ | |
| '{ | |
| version: $version, | |
| notes: $notes, | |
| pub_date: $pub_date, | |
| platforms: { | |
| "darwin-aarch64": { signature: $mac_arm_sig, url: $mac_arm_url }, | |
| "darwin-x86_64": { signature: $mac_intel_sig, url: $mac_intel_url }, | |
| "windows-x86_64": { signature: $win_msi_sig, url: $win_msi_url }, | |
| "windows-x86_64-msi": { signature: $win_msi_sig, url: $win_msi_url }, | |
| "windows-x86_64-nsis": { signature: $win_nsis_sig, url: $win_nsis_url }, | |
| "linux-x86_64": { signature: $linux_appimage_sig, url: $linux_appimage_url }, | |
| "linux-x86_64-appimage": { signature: $linux_appimage_sig, url: $linux_appimage_url }, | |
| "linux-x86_64-deb": { signature: $linux_deb_sig, url: $linux_deb_url }, | |
| "linux-x86_64-rpm": { signature: $linux_rpm_sig, url: $linux_rpm_url } | |
| } | |
| }' > "$tmp/latest.json" | |
| gh release upload "$RELEASE_TAG" "$tmp/latest.json" --repo "$GITHUB_REPOSITORY" --clobber | |
| gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --notes-file "$release_notes" | |
| canary-release: | |
| name: Canary release assets | |
| runs-on: ubuntu-22.04 | |
| needs: publish-updater-json | |
| if: ${{ needs.publish-updater-json.result == 'success' }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| - name: Download canary assets | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| tmp="$(mktemp -d)" | |
| trap 'rm -rf "$tmp"' EXIT | |
| version="${RELEASE_TAG#v}" | |
| assets_file="$tmp/assets.txt" | |
| assets_json="$tmp/assets.json" | |
| release_body="$tmp/release-body.md" | |
| mkdir -p release-verification | |
| report="release-verification/release-canary-verification.json" | |
| gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json assets > "$assets_json" | |
| gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json body -q .body > "$release_body" | |
| jq -r '.assets[].name' "$assets_json" > "$assets_file" | |
| sample_assets="$(grep -E 'aarch64\.app\.tar\.gz$' "$assets_file" || true)" | |
| sample_asset_count="$(printf '%s\n' "$sample_assets" | sed '/^$/d' | wc -l | tr -d ' ')" | |
| macos_updater_assets="$( | |
| grep -E '\.app\.tar\.gz$' "$assets_file" || true | |
| )" | |
| if [ "$sample_asset_count" -eq 0 ]; then | |
| echo "::error::Could not find macOS Apple Silicon updater asset." | |
| exit 1 | |
| fi | |
| if [ "$sample_asset_count" -gt 1 ]; then | |
| echo "::error::Multiple macOS Apple Silicon updater assets found." >&2 | |
| printf '%s\n' "$sample_assets" >&2 | |
| exit 1 | |
| fi | |
| sample_asset="$sample_assets" | |
| if [ -z "$macos_updater_assets" ]; then | |
| echo "::error::Could not find any macOS updater app tarball assets." | |
| exit 1 | |
| fi | |
| gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "latest.json" --dir "$tmp" | |
| gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "SHA256SUMS" --dir "$tmp" | |
| gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "*.sig" --dir "$tmp" | |
| while IFS= read -r updater_asset; do | |
| if [ -z "$updater_asset" ]; then | |
| continue | |
| fi | |
| gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern "$updater_asset" --dir "$tmp" | |
| done <<< "$macos_updater_assets" | |
| python scripts/verify_release_canary.py \ | |
| --release-dir "$tmp" \ | |
| --sample-asset "$sample_asset" \ | |
| --expected-version "$version" \ | |
| --assets-json "$assets_json" \ | |
| --release-body-file "$release_body" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --tag "$RELEASE_TAG" | tee "$report" | |
| { | |
| echo "### Release canary verification" | |
| echo '```json' | |
| cat "$report" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload release canary verification report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-canary-verification | |
| path: release-verification/release-canary-verification.json | |
| if-no-files-found: error | |
| canary-installers: | |
| name: Canary installer downloads (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.os }} | |
| needs: publish-updater-json | |
| if: ${{ needs.publish-updater-json.result == 'success' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: mac-apple-silicon | |
| os: macos-14 | |
| verifier-args: --verify-dmg --launch-dmg-app --verify-dmg-installed-copy --verify-macos-gatekeeper --sample-platform darwin-aarch64 | |
| - platform: mac-intel | |
| os: macos-15-intel | |
| verifier-args: --verify-dmg --launch-dmg-app --verify-dmg-installed-copy --verify-macos-gatekeeper --sample-platform darwin-x86_64 | |
| - platform: linux | |
| os: ubuntu-22.04 | |
| verifier-args: --skip-dmg --verify-linux-installers --launch-linux-packages --launch-linux-appimage | |
| - platform: windows | |
| os: windows-latest | |
| verifier-args: --skip-dmg --verify-windows-installer --launch-windows-msi --launch-windows-nsis | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| - name: Install Linux verifier tools | |
| if: matrix.platform == 'linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev rpm cpio xvfb | |
| - name: Download and verify draft installer assets | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| mkdir -p release-verification | |
| report="release-verification/github-release-verification-draft-${{ matrix.platform }}.json" | |
| if [ "${{ matrix.platform }}" = "linux" ]; then | |
| xvfb-run -a python scripts/verify_github_release.py \ | |
| "$RELEASE_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --expected-version "$version" \ | |
| --launch-timeout 90 \ | |
| ${{ matrix.verifier-args }} | tee "$report" | |
| else | |
| python scripts/verify_github_release.py \ | |
| "$RELEASE_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --expected-version "$version" \ | |
| --launch-timeout 90 \ | |
| ${{ matrix.verifier-args }} | tee "$report" | |
| fi | |
| { | |
| echo "### Draft installer verification: ${{ matrix.platform }}" | |
| echo "#### Verification summary" | |
| python - "$report" <<'PY' | |
| import json | |
| import sys | |
| with open(sys.argv[1], encoding="utf-8") as handle: | |
| report = json.load(handle) | |
| if not isinstance(report.get("verification_summary"), dict): | |
| raise SystemExit("verification_summary missing from release verification report") | |
| checks = report['verification_summary'].get('checks') | |
| if not isinstance(checks, dict): | |
| raise SystemExit("verification_summary checks missing from release verification report") | |
| failed_checks = sorted(name for name, ok in checks.items() if ok is not True) | |
| if failed_checks: | |
| raise SystemExit("verification_summary checks failed: " + ", ".join(failed_checks)) | |
| if report['verification_summary'].get('ready_for_public_download') is not True: | |
| raise SystemExit("verification_summary ready_for_public_download is not true") | |
| print("```json") | |
| print(json.dumps(report['verification_summary'], indent=2, sort_keys=True)) | |
| print("```") | |
| PY | |
| echo "#### Full report" | |
| echo '```json' | |
| cat "$report" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload draft installer verification report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: draft-installer-verification-${{ matrix.platform }} | |
| path: release-verification/*.json | |
| if-no-files-found: error | |
| publish-release: | |
| name: Publish release | |
| runs-on: ubuntu-22.04 | |
| needs: | |
| - canary-release | |
| - canary-installers | |
| if: ${{ needs.canary-release.result == 'success' && needs.canary-installers.result == 'success' }} | |
| steps: | |
| - name: Publish draft release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| gh release edit "$RELEASE_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --draft=false \ | |
| --prerelease=false \ | |
| --latest | |
| verify-live-release: | |
| name: Verify live GitHub downloads (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.os }} | |
| needs: publish-release | |
| if: ${{ needs.publish-release.result == 'success' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: mac-apple-silicon | |
| os: macos-14 | |
| verifier-args: --verify-dmg --launch-dmg-app --verify-dmg-installed-copy --verify-macos-gatekeeper --sample-platform darwin-aarch64 | |
| - platform: mac-intel | |
| os: macos-15-intel | |
| verifier-args: --verify-dmg --launch-dmg-app --verify-dmg-installed-copy --verify-macos-gatekeeper --sample-platform darwin-x86_64 | |
| - platform: linux | |
| os: ubuntu-22.04 | |
| verifier-args: --skip-dmg --verify-linux-installers --launch-linux-packages --launch-linux-appimage | |
| - platform: windows | |
| os: windows-latest | |
| verifier-args: --skip-dmg --verify-windows-installer --launch-windows-msi --launch-windows-nsis | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| - name: Install Linux verifier tools | |
| if: matrix.platform == 'linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev rpm cpio xvfb | |
| - name: Download and verify published assets | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${RELEASE_TAG#v}" | |
| mkdir -p release-verification | |
| report="release-verification/github-release-verification-live-${{ matrix.platform }}.json" | |
| if [ "${{ matrix.platform }}" = "linux" ]; then | |
| xvfb-run -a python scripts/verify_github_release.py \ | |
| "$RELEASE_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --expected-version "$version" \ | |
| --require-latest \ | |
| --require-public \ | |
| --launch-timeout 90 \ | |
| ${{ matrix.verifier-args }} | tee "$report" | |
| else | |
| python scripts/verify_github_release.py \ | |
| "$RELEASE_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --expected-version "$version" \ | |
| --require-latest \ | |
| --require-public \ | |
| --launch-timeout 90 \ | |
| ${{ matrix.verifier-args }} | tee "$report" | |
| fi | |
| { | |
| echo "### Live installer verification: ${{ matrix.platform }}" | |
| echo "#### Verification summary" | |
| python - "$report" <<'PY' | |
| import json | |
| import sys | |
| with open(sys.argv[1], encoding="utf-8") as handle: | |
| report = json.load(handle) | |
| if not isinstance(report.get("verification_summary"), dict): | |
| raise SystemExit("verification_summary missing from release verification report") | |
| checks = report['verification_summary'].get('checks') | |
| if not isinstance(checks, dict): | |
| raise SystemExit("verification_summary checks missing from release verification report") | |
| failed_checks = sorted(name for name, ok in checks.items() if ok is not True) | |
| if failed_checks: | |
| raise SystemExit("verification_summary checks failed: " + ", ".join(failed_checks)) | |
| if report['verification_summary'].get('ready_for_public_download') is not True: | |
| raise SystemExit("verification_summary ready_for_public_download is not true") | |
| print("```json") | |
| print(json.dumps(report['verification_summary'], indent=2, sort_keys=True)) | |
| print("```") | |
| PY | |
| echo "#### Full report" | |
| echo '```json' | |
| cat "$report" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload live installer verification report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: live-installer-verification-${{ matrix.platform }} | |
| path: release-verification/*.json | |
| if-no-files-found: error | |
| rollback-live-release: | |
| name: Roll back failed live release | |
| runs-on: ubuntu-22.04 | |
| needs: | |
| - publish-release | |
| - verify-live-release | |
| if: ${{ always() && needs.publish-release.result == 'success' && needs.verify-live-release.result != 'success' }} | |
| steps: | |
| - name: Rollback failed live release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| gh release edit "$RELEASE_TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --draft=true \ | |
| --prerelease=true | |
| { | |
| echo "### Live release rolled back" | |
| echo "Live GitHub download verification did not pass, so $RELEASE_TAG was returned to draft/prerelease state." | |
| } >> "$GITHUB_STEP_SUMMARY" |