name: Release on: push: tags: ["v*"] workflow_dispatch: inputs: version: description: "Release version (e.g. 1.0.0)" required: true type: string permissions: contents: read concurrency: group: release-${{ github.ref_name }} cancel-in-progress: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true PNPM_CONFIG_MANAGE_PACKAGE_MANAGER_VERSIONS: "false" jobs: prepare-release: name: Prepare release runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: write # read draft releases (release-please creates the release as a draft; the GitHub API hides drafts from tokens with only contents: read) outputs: version: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.tag }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Extract version id: version env: EVENT_NAME: ${{ github.event_name }} INPUT_VERSION: ${{ github.event.inputs.version }} run: | if [ "${EVENT_NAME}" = "push" ]; then tag="${GITHUB_REF#refs/tags/}" version="${tag#v}" else version="${INPUT_VERSION}" tag="v${version}" fi echo "version=${version}" >> "$GITHUB_OUTPUT" echo "tag=${tag}" >> "$GITHUB_OUTPUT" - name: Verify package.json version matches release tag env: EXPECTED_VERSION: ${{ steps.version.outputs.version }} RELEASE_TAG: ${{ steps.version.outputs.tag }} run: | set -euo pipefail package_version="$(node -p "require('./package.json').version")" if [ "${package_version}" != "${EXPECTED_VERSION}" ]; then echo "::error::Release ${RELEASE_TAG} expects version ${EXPECTED_VERSION}, but package.json is ${package_version}." echo "The published bundle takes its version from package.json (scripts/sync-version.mjs), so a tag that disagrees with package.json builds assets named OpenKara_${package_version}_* while the winget/flatpak manifests look for OpenKara_${EXPECTED_VERSION}_* and the release fails." echo "To fix: bump package.json to ${EXPECTED_VERSION}, run 'pnpm version:sync', commit, then re-tag that commit." exit 1 fi echo "package.json version ${package_version} matches release ${RELEASE_TAG}." - name: Verify draft release env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ steps.version.outputs.tag }} run: | set -euo pipefail release_id="$(gh api "repos/${GH_REPO}/releases" --paginate \ --jq "[.[] | select(.tag_name == \"${RELEASE_TAG}\" and .draft == true)] | .[0].id // empty")" if [ -z "${release_id}" ]; then echo "::error::No draft GitHub Release is bound to ${RELEASE_TAG}. Run the Release Please workflow before dispatching Release." exit 1 fi echo "Verified draft release ${release_id} for ${RELEASE_TAG}." verify-nightly-evidence: name: Verify Nightly evidence needs: prepare-release runs-on: ubuntu-latest # Budget: checkout/setup + dispatch (~1-2m), then a 44-minute evidence # poll (real Nightly runs take 43+ minutes), then conclusion + download. # Must stay above the poll deadline so the poll's own error surfaces # instead of the runner killing the job mid-loop. timeout-minutes: 50 concurrency: group: verify-nightly-evidence-${{ github.repository }}-${{ github.sha }} cancel-in-progress: false permissions: actions: write # Dispatch same-commit Nightly run when no recent evidence exists. contents: read # Checks out the evidence verifier. steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 package-manager-cache: false # Release workflow: disable cache to prevent poisoning. - name: Resolve or build candidate Nightly evidence env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} EVIDENCE_DIR: ${{ runner.temp }}/nightly-evidence run: | set -euo pipefail mkdir -p "${EVIDENCE_DIR}" fresh_run() { gh run list \ --repo "${GH_REPO}" \ --workflow nightly-hardening.yml \ --commit "${GITHUB_SHA}" \ --status success \ --limit 1 \ --json databaseId,createdAt \ --jq "[.[] | select(.createdAt >= \"$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)\")] | .[0].databaseId // empty" } reused_run="$(fresh_run)" if [ -n "${reused_run}" ]; then echo "Reusing successful Nightly run ${reused_run} for ${GITHUB_SHA}." gh run download "${reused_run}" \ --repo "${GH_REPO}" \ --name nightly-evidence \ --dir "${EVIDENCE_DIR}" exit 0 fi # No fresh reusable evidence. Dispatch Nightly on the release commit, # guarding against an already-in-flight run so a retried Release does # not double-dispatch. in_flight="$(gh run list \ --repo "${GH_REPO}" \ --workflow nightly-hardening.yml \ --commit "${GITHUB_SHA}" \ --limit 5 \ --json databaseId,status \ --jq '[.[] | select(.status == "queued" or .status == "in_progress" or .status == "pending" or .status == "waiting")] | .[0].databaseId // empty')" if [ -z "${in_flight}" ]; then echo "No recent Nightly evidence and no in-flight run; dispatching nightly-hardening for ${GITHUB_SHA} on ref ${GITHUB_REF_NAME}." # workflow_dispatch rejects bare SHAs; release runs carry the tag ref. gh workflow run nightly-hardening.yml \ --repo "${GH_REPO}" \ --ref "${GITHUB_REF_NAME}" else echo "Nightly run ${in_flight} already in flight for ${GITHUB_SHA}; waiting on it." fi completed_run() { gh run list \ --repo "${GH_REPO}" \ --workflow nightly-hardening.yml \ --commit "${GITHUB_SHA}" \ --limit 1 \ --json databaseId,status,conclusion \ --jq '.[0] | select(.status == "completed") | .databaseId // empty' } # Real Nightly runs have taken 43+ minutes; a shorter deadline fails # cold releases that would still succeed within the job budget. The # job's timeout-minutes reserves headroom above this for the setup # steps before the poll and the download after it. poll_minutes=44 deadline=$(( $(date +%s) + poll_minutes * 60 )) nightly_run="" while [ "$(date +%s)" -lt "${deadline}" ]; do nightly_run="$(completed_run)" if [ -n "${nightly_run}" ]; then break fi sleep 30 done # The loop sleeps after each check, so a run that completes in the # final interval would otherwise be reported as a timeout. Check # once more before failing. if [ -z "${nightly_run}" ]; then nightly_run="$(completed_run)" fi if [ -z "${nightly_run}" ]; then echo "::error::Dispatched Nightly run did not finish within ${poll_minutes} minutes for commit ${GITHUB_SHA}." exit 1 fi conclusion="$(gh run view "${nightly_run}" \ --repo "${GH_REPO}" \ --json conclusion \ --jq '.conclusion')" if [ "${conclusion}" != "success" ]; then echo "::error::Nightly run ${nightly_run} for commit ${GITHUB_SHA} ended with ${conclusion}. Fix the failure before releasing." exit 1 fi gh run download "${nightly_run}" \ --repo "${GH_REPO}" \ --name nightly-evidence \ --dir "${EVIDENCE_DIR}" - name: Verify candidate Nightly evidence run: | node scripts/ci/nightly-evidence.mjs verify \ --input "${RUNNER_TEMP}/nightly-evidence/nightly-evidence.json" \ --commit "${GITHUB_SHA}" \ --max-age-hours 24 # Release gate profile (docs/CI_LAYERS.md): # - Windows: full clean/upgrade + #284 fault recovery + separation after heal # - macOS/Linux installed-app: install lifecycle on primary arch only # - separation: one primary arch sanity (Linux x64 + macOS AS); full 4-arch # lives on Nightly and may be reused for the same SHA within 24h # - publish: multi-arch signed packages + updater + checksums release-separation-smoke: name: Release separation smoke (${{ matrix.name }}) needs: [prepare-release, verify-nightly-evidence] strategy: fail-fast: false matrix: include: - name: Linux x64 os: ubuntu-22.04 ort_target: x86_64-unknown-linux-gnu artifact_prefix: release-separation-smoke-x86_64-linux - name: macOS Apple Silicon os: macos-14 ort_target: aarch64-apple-darwin artifact_prefix: release-separation-smoke-aarch64-apple uses: ./.github/workflows/reusable-separation-smoke.yml with: name: ${{ matrix.name }} os: ${{ matrix.os }} ort_target: ${{ matrix.ort_target }} artifact_prefix: ${{ matrix.artifact_prefix }} retention_days: 90 release-windows-installed-smoke: # Release-only lifecycle proof on Windows (NSIS + UIA + upgrade + faults). # Display scaling matrix stays on Nightly to keep release wall-clock shorter. name: Release Windows installed-app smoke needs: [prepare-release, verify-nightly-evidence] uses: ./.github/workflows/reusable-windows-installed-app.yml with: openkara_version: ${{ needs.prepare-release.outputs.version }} artifact_prefix: release-windows-installed-app driver_scenario: clean-install uia_scenario: keyboard-workflow run_keyboard_uia: true run_fault_injection: true run_runtime_bootstrap_regression: true run_display_matrix: false previous_version: "" retention_days: 90 release_build: true release_tag: ${{ needs.prepare-release.outputs.tag }} updater_target: windows-x86_64 secrets: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} permissions: contents: read release-macos-installed-smoke: # One arch for install lifecycle. Publish still builds Intel + AS installers. name: Release macOS installed-app smoke (Apple Silicon) needs: [prepare-release, verify-nightly-evidence] uses: ./.github/workflows/reusable-macos-installed-app-smoke.yml with: openkara_version: ${{ needs.prepare-release.outputs.version }} artifact_prefix: release-macos-installed-app-aarch64 os: macos-14 rust_target: aarch64-apple-darwin tauri_target: aarch64-apple-darwin scenario: clean-install previous_version: none retention_days: 90 release_build: true release_tag: ${{ needs.prepare-release.outputs.tag }} updater_target: darwin-aarch64 secrets: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} permissions: contents: read release-macos-intel-installed-smoke: name: Release macOS installed-app smoke (Intel) needs: [prepare-release, verify-nightly-evidence] uses: ./.github/workflows/reusable-macos-installed-app-smoke.yml with: openkara_version: ${{ needs.prepare-release.outputs.version }} artifact_prefix: release-macos-installed-app-x86_64 os: macos-15-intel rust_target: x86_64-apple-darwin tauri_target: x86_64-apple-darwin scenario: clean-install previous_version: none retention_days: 90 release_build: true release_tag: ${{ needs.prepare-release.outputs.tag }} updater_target: darwin-x86_64 secrets: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} permissions: contents: read release-linux-installed-smoke: name: Release Linux installed-app smoke (x64) needs: [prepare-release, verify-nightly-evidence] uses: ./.github/workflows/reusable-linux-installed-app-smoke.yml with: openkara_version: ${{ needs.prepare-release.outputs.version }} artifact_prefix: release-linux-installed-app-x86_64 os: ubuntu-22.04 rust_target: x86_64-unknown-linux-gnu tauri_target: x86_64-unknown-linux-gnu scenario: clean-install previous_version: none retention_days: 90 release_build: true release_tag: ${{ needs.prepare-release.outputs.tag }} updater_target: linux-x86_64 secrets: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} permissions: contents: read assemble-release-evidence: name: Assemble release evidence runs-on: ubuntu-latest timeout-minutes: 15 needs: - prepare-release - release-windows-installed-smoke - release-macos-installed-smoke - release-macos-intel-installed-smoke - release-linux-installed-smoke permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Download Rust evidence driver uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release-linux-installed-app-x86_64-evidence-driver path: ${{ runner.temp }}/evidence-driver - name: Download installed-app evidence fragments uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: release-*-smoke-reports path: ${{ runner.temp }}/smoke-reports - name: Download available bundle size reports if: ${{ always() }} continue-on-error: true uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: release-*-size-report path: ${{ runner.temp }}/size-reports merge-multiple: true - name: Aggregate canonical release evidence env: RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }} RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }} EVIDENCE_ROOT: ${{ runner.temp }}/evidence run: | set -euo pipefail mkdir -p "$EVIDENCE_ROOT" mapfile -t fragments < <(find "${RUNNER_TEMP}/smoke-reports" -name "release-evidence-fragment.json" -type f | sort) fragment_args=() for fragment in "${fragments[@]}"; do fragment_args+=(--fragment "$fragment") done chmod +x "${RUNNER_TEMP}/evidence-driver/openkara_release_evidence" "${RUNNER_TEMP}/evidence-driver/openkara_release_evidence" aggregate \ --repository "${GITHUB_REPOSITORY}" \ --commit-sha "${GITHUB_SHA}" \ --tag "$RELEASE_TAG" \ --version "$RELEASE_VERSION" \ "${fragment_args[@]}" \ --output "$EVIDENCE_ROOT/release-evidence.json" - name: Upload canonical release evidence uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-evidence path: ${{ runner.temp }}/evidence/release-evidence.json if-no-files-found: error retention-days: 90 - name: Merge bundle size reports if: ${{ always() }} continue-on-error: true run: | node scripts/ci/merge-size-reports.mjs \ "${RUNNER_TEMP}/size-reports" \ "${RUNNER_TEMP}/evidence/release-size-report.json" - name: Upload bundle size report if: ${{ always() }} continue-on-error: true uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-size-report path: ${{ runner.temp }}/evidence/release-size-report.json if-no-files-found: ignore retention-days: 90 publish-release-evidence: name: Publish release evidence runs-on: ubuntu-latest timeout-minutes: 10 needs: [prepare-release, assemble-release-evidence] permissions: contents: write # Uploads the canonical release evidence to the draft release. steps: - name: Download canonical release evidence uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release-evidence path: ${{ runner.temp }}/release-evidence - name: Upload release evidence env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }} run: | set -euo pipefail gh release upload "$RELEASE_TAG" --repo "$GH_REPO" \ "${RUNNER_TEMP}/release-evidence/release-evidence.json" --clobber - name: Download bundle size report if: ${{ always() }} continue-on-error: true uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release-size-report path: ${{ runner.temp }}/release-size-report - name: Upload bundle size report if: ${{ always() }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }} run: | set -euo pipefail report="${RUNNER_TEMP}/release-size-report/release-size-report.json" if [ -f "$report" ]; then gh release upload "$RELEASE_TAG" --repo "$GH_REPO" "$report" --clobber else echo "Bundle size evidence was unavailable; canonical release evidence was already published." fi # Playwright accessibility is exercised in nightly-hardening (windows-matrix # accessibility leg) and local `pnpm test:a11y`. Release publish is gated on # the native/installed-app smoke jobs below; do not ship without them. publish: name: Release (${{ matrix.name }}) runs-on: ${{ matrix.os }} timeout-minutes: 90 needs: - prepare-release - release-separation-smoke - release-windows-installed-smoke - release-macos-installed-smoke - release-macos-intel-installed-smoke - release-linux-installed-smoke - publish-release-evidence permissions: contents: write # Creates the draft release and uploads tested candidates. strategy: fail-fast: false matrix: include: - name: macOS (Apple Silicon) os: macos-14 artifact_prefix: release-macos-installed-app-aarch64 ort_target: aarch64-apple-darwin rust_targets: aarch64-apple-darwin release_target: darwin-aarch64 # Native Intel runner: the Intel app must be built, and its runtime # loaded, on the architecture it ships for — cross-compiling from # Apple Silicon proves neither. macos-15-intel is GitHub's supported # x86_64 image through August 2027; a self-hosted Intel replacement # must be established before hosted Intel runners retire. - name: macOS (Intel) os: macos-15-intel artifact_prefix: release-macos-installed-app-x86_64 ort_target: x86_64-apple-darwin rust_targets: x86_64-apple-darwin release_target: darwin-x86_64 - name: Linux os: ubuntu-22.04 artifact_prefix: release-linux-installed-app-x86_64 ort_target: x86_64-unknown-linux-gnu rust_targets: "" release_target: linux-x86_64 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 package-manager-cache: false # release workflow: disable cache to prevent poisoning - name: Setup pnpm if: runner.os != 'macOS' || runner.arch != 'X64' uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: version: 12.15.0 standalone: true - name: Setup pnpm (macOS Intel) if: runner.os == 'macOS' && runner.arch == 'X64' # Intel macOS needs the pinned pnpm CLI: standalone binaries require newer macOS. run: npm install -g pnpm@12.15.0 # zizmor: ignore[adhoc-packages] - name: Setup Rust uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master with: toolchain: stable targets: ${{ matrix.rust_targets }} - name: Restore Rust cache uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ${{ runner.os == 'Windows' && 'openkara-windows-release' || format('{0}-{1}-release', github.workflow, matrix.os) }} key: ${{ runner.os == 'Windows' && github.sha || matrix.os }} workspaces: ./src-tauri -> target lookup-only: true # release workflow: restore only, never save - name: Install Linux build dependencies if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y \ build-essential \ curl \ file \ patchelf \ libasound2-dev \ libayatana-appindicator3-dev \ librsvg2-dev \ libssl-dev \ libwebkit2gtk-4.1-dev \ libxdo-dev \ wget - name: Restore ONNX Runtime cache uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: src-tauri/generated/onnxruntime key: onnxruntime-${{ matrix.ort_target }}-${{ hashFiles('scripts/prepare-onnx-runtime.mjs', 'src-tauri/catalog/release-manifest.json') }} lookup-only: true # release workflow: restore only, never save - name: Prepare ONNX Runtime env: ORT_TARGET: ${{ matrix.ort_target }} shell: bash run: | node scripts/prepare-onnx-runtime.mjs --target "${ORT_TARGET}" - name: Install JavaScript dependencies run: pnpm install --frozen-lockfile - name: Audit dependencies run: pnpm audit --audit-level=high - name: Download tested Release Candidate uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: ${{ matrix.artifact_prefix }}-* path: ${{ runner.temp }}/release-candidate merge-multiple: true - name: Download release evidence and verifier uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release-evidence path: ${{ runner.temp }}/release-evidence - name: Download platform release evidence verifier uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ matrix.artifact_prefix }}-evidence-driver path: ${{ runner.temp }}/evidence-driver - name: Verify tested Release Candidate bytes shell: bash env: CANDIDATE_ROOT: ${{ runner.temp }}/release-candidate EVIDENCE_ROOT: ${{ runner.temp }}/release-evidence DRIVER_ROOT: ${{ runner.temp }}/evidence-driver RELEASE_TARGET: ${{ matrix.release_target }} run: | set -euo pipefail driver="${DRIVER_ROOT}/openkara_release_evidence" if [ ! -x "$driver" ]; then chmod +x "$driver" fi "$driver" verify-assets \ --evidence "${EVIDENCE_ROOT}/release-evidence.json" \ --target "$RELEASE_TARGET" \ --assets-dir "$CANDIDATE_ROOT" - name: Publish tested Release Candidate shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }} CANDIDATE_ROOT: ${{ runner.temp }}/release-candidate run: | set -euo pipefail files=() while IFS= read -r -d '' file; do files+=("$file") done < <(find "$CANDIDATE_ROOT" -type f \( \ -name "*.dmg" -o \ -name "*.AppImage" -o \ -name "*.deb" -o \ -name "*.tar.gz" -o \ -name "*.sig" \ \) -print0) if [ "${#files[@]}" -eq 0 ]; then echo "::error::No tested release candidate assets were found." exit 1 fi gh release upload "$RELEASE_TAG" --repo "$GH_REPO" "${files[@]}" --clobber publish-windows: name: Release (Windows) runs-on: windows-latest timeout-minutes: 30 needs: - prepare-release - publish - release-windows-installed-smoke permissions: contents: write # Needed to upload Windows release assets to the draft GitHub Release. steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 package-manager-cache: false - name: Download Windows smoke artifacts uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: path: ${{ runner.temp }}/windows-smoke pattern: release-windows-installed-app-* - name: Download canonical release evidence uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release-evidence path: ${{ runner.temp }}/release-evidence - name: Verify tested Windows Release Candidate bytes shell: pwsh env: WINDOWS_SMOKE_ROOT: ${{ runner.temp }}\windows-smoke EVIDENCE_ROOT: ${{ runner.temp }}\release-evidence run: | $ErrorActionPreference = "Stop" $evidence = Get-ChildItem -Path $env:WINDOWS_SMOKE_ROOT -Filter "openkara_release_evidence.exe" -File -Recurse | Select-Object -First 1 if ($null -eq $evidence) { throw "Windows release evidence verifier was not found" } $canonical = Join-Path $env:EVIDENCE_ROOT "release-evidence.json" if (-not (Test-Path -Path $canonical -PathType Leaf)) { throw "Canonical release evidence was not found" } & $evidence.FullName verify-assets --evidence $canonical --target windows-x86_64 --assets-dir $env:WINDOWS_SMOKE_ROOT if ($LASTEXITCODE -ne 0) { throw "Windows Release Candidate bytes do not match canonical release evidence" } - name: Publish Windows release assets shell: pwsh env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }} RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }} run: | $ErrorActionPreference = "Stop" $installer = Get-ChildItem -Path "${env:RUNNER_TEMP}\windows-smoke\release-windows-installed-app-installer" -Filter "*setup.exe" -File -Recurse | Select-Object -First 1 if ($null -eq $installer) { throw "No Windows installer artifact found" } $updaterFiles = @(Get-ChildItem -Path "${env:RUNNER_TEMP}\windows-smoke\release-windows-installed-app-updater" -File -Recurse | Where-Object { $_.Name -like "*setup.exe.sig" }) if ($updaterFiles.Count -eq 0) { throw "No Windows updater artifacts found" } gh release upload $env:RELEASE_TAG --repo $env:GH_REPO $installer.FullName --clobber foreach ($updaterFile in $updaterFiles) { gh release upload $env:RELEASE_TAG --repo $env:GH_REPO $updaterFile.FullName --clobber } generate-release-metadata: name: Generate release metadata runs-on: ubuntu-latest timeout-minutes: 15 needs: [prepare-release, publish, publish-windows, publish-release-evidence] permissions: contents: write # Needed to attach evidence-derived release metadata. steps: - name: Download Rust evidence driver uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release-linux-installed-app-x86_64-evidence-driver path: ${{ runner.temp }}/evidence-driver - name: Download release evidence and published assets env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }} run: | set -euo pipefail mkdir -p artifacts gh release download "${RELEASE_TAG}" --pattern "release-evidence.json" --dir artifacts --clobber gh release download "${RELEASE_TAG}" --pattern "OpenKara*" --dir artifacts --clobber driver="${RUNNER_TEMP}/evidence-driver/openkara_release_evidence" chmod +x "$driver" "$driver" latest \ --evidence artifacts/release-evidence.json \ --output latest.json "$driver" checksums \ --evidence artifacts/release-evidence.json \ --assets-dir artifacts \ --output SHA256SUMS gh release upload "${RELEASE_TAG}" latest.json SHA256SUMS --clobber # Ship gate: merge of the release-please PR is the human decision. After # platform smoke, signed assets, and SHA256SUMS land, undraft so # /releases/latest and distribution submits see a public release. publish-github-release: name: Publish GitHub Release runs-on: ubuntu-latest timeout-minutes: 5 needs: [prepare-release, publish, publish-windows, generate-release-metadata] permissions: contents: write # undraft the GitHub Release after assets are complete steps: - name: Publish release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }} RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }} run: | set -euo pipefail required_assets=( "OpenKara_${RELEASE_VERSION}_x64-setup.exe" "OpenKara_${RELEASE_VERSION}_aarch64.dmg" "OpenKara_${RELEASE_VERSION}_x64.dmg" "OpenKara_${RELEASE_VERSION}_amd64.AppImage" "OpenKara_${RELEASE_VERSION}_amd64.deb" "release-evidence.json" "latest.json" "SHA256SUMS" ) asset_names="$(gh release view "${RELEASE_TAG}" --repo "${GH_REPO}" --json assets --jq '[.assets[].name] | join("\n")')" missing=0 for name in "${required_assets[@]}"; do if ! printf '%s\n' "${asset_names}" | grep -Fxq "${name}"; then echo "::error::Release ${RELEASE_TAG} is missing required asset ${name}" missing=1 fi done if [ "${missing}" -ne 0 ]; then exit 1 fi # releaseDraft stayed true while legs uploaded; undraft only after # the full asset set is present. Prerelease stays set from the tag # (vX.Y.Z-rc.1) so /releases/latest still ignores those tags. gh release edit "${RELEASE_TAG}" --repo "${GH_REPO}" --draft=false echo "Published ${RELEASE_TAG} (assets verified)" prepare-distribution-manifests: name: Prepare distribution manifests runs-on: ubuntu-latest timeout-minutes: 15 needs: [prepare-release, publish-github-release] permissions: contents: write # upload distribution manifests to the published GitHub Release env: GITHUB_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Ensure release source tag exists if: github.event_name == 'workflow_dispatch' env: RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }} run: | set -euo pipefail tag="v${RELEASE_VERSION}" git fetch --tags origin if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then tagged_sha="$(git rev-list -n 1 "${tag}")" if [ "${tagged_sha}" != "${GITHUB_SHA}" ]; then echo "::error::${tag} already points at ${tagged_sha}, expected ${GITHUB_SHA}." exit 1 fi exit 0 fi git tag "${tag}" "${GITHUB_SHA}" auth_header="$(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 -w0)" git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ push origin "refs/tags/${tag}" - name: Setup Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 package-manager-cache: false # release workflow: disable cache to prevent poisoning # Distinct output directories and artifact names, so the two renders # and the two uploads do not share files. - parallel: - name: Render WinGet manifests env: RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }} run: | node scripts/render-winget-manifests.mjs \ --version "${RELEASE_VERSION}" \ --output "${GITHUB_WORKSPACE}/dist/winget" - name: Render Flatpak manifests env: RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }} run: | node scripts/render-flatpak-manifest.mjs \ --version "${RELEASE_VERSION}" \ --output "${GITHUB_WORKSPACE}/dist/flatpak" - parallel: - name: Upload WinGet manifest artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: winget-manifests path: dist/winget - name: Upload Flatpak manifest artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: flatpak-manifests path: dist/flatpak submit-winget-pr: name: Submit WinGet PR runs-on: ubuntu-latest timeout-minutes: 15 needs: [prepare-release, prepare-distribution-manifests] if: ${{ vars.WINGET_FORK_REPO != '' }} steps: - name: Check WinGet automation bootstrap id: bootstrap env: GH_TOKEN: ${{ secrets.WINGET_PR_TOKEN }} run: | if [ -z "${GH_TOKEN}" ]; then echo "::error::WINGET_PR_TOKEN is required when WINGET_FORK_REPO is configured." exit 1 fi echo "ready=true" >> "$GITHUB_OUTPUT" - name: Download WinGet manifest artifact if: ${{ steps.bootstrap.outputs.ready == 'true' }} uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: winget-manifests path: dist/winget - name: Create or update WinGet PR if: ${{ steps.bootstrap.outputs.ready == 'true' }} env: GH_TOKEN: ${{ secrets.WINGET_PR_TOKEN }} WINGET_FORK_REPO: ${{ vars.WINGET_FORK_REPO }} WINGET_TARGET_REPO: ${{ vars.WINGET_TARGET_REPO || 'microsoft/winget-pkgs' }} WINGET_BASE_BRANCH: ${{ vars.WINGET_BASE_BRANCH || 'master' }} WINGET_PACKAGE_IDENTIFIER: ${{ vars.WINGET_PACKAGE_IDENTIFIER || 'thedavidweng.OpenKara' }} VERSION: ${{ needs.prepare-release.outputs.version }} run: | set -euo pipefail branch="openkara/${VERSION}" fork_owner="${WINGET_FORK_REPO%%/*}" target_repo="${WINGET_TARGET_REPO}" base_branch="${WINGET_BASE_BRANCH}" git clone "https://x-access-token:${GH_TOKEN}@github.com/${WINGET_FORK_REPO}.git" winget-fork cd winget-fork git remote add upstream "https://github.com/${target_repo}.git" git fetch upstream "${base_branch}" git checkout -B "${branch}" "upstream/${base_branch}" mkdir -p manifests/t/thedavidweng/OpenKara cp -R ../dist/winget/t/thedavidweng/OpenKara/"${VERSION}" manifests/t/thedavidweng/OpenKara/ git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add manifests/t/thedavidweng/OpenKara/"${VERSION}" if git diff --cached --quiet; then echo "No WinGet manifest changes to submit" exit 0 fi git commit -m "Add OpenKara ${VERSION}" git push origin "${branch}" --force pr_url="$(gh pr list --repo "${target_repo}" --head "${fork_owner}:${branch}" --json url --jq '.[0].url')" if [ -n "${pr_url}" ]; then echo "Existing WinGet PR: ${pr_url}" exit 0 fi gh pr create \ --repo "${target_repo}" \ --base "${base_branch}" \ --head "${fork_owner}:${branch}" \ --title "New version: ${WINGET_PACKAGE_IDENTIFIER} version ${VERSION}" \ --body "Automated WinGet manifest update for OpenKara ${VERSION}." submit-flathub-pr: name: Prepare or submit Flathub PR runs-on: ubuntu-latest timeout-minutes: 15 needs: [prepare-release, prepare-distribution-manifests] if: ${{ vars.FLATHUB_FORK_REPO != '' }} steps: - name: Check Flathub automation bootstrap id: bootstrap env: GH_TOKEN: ${{ secrets.FLATHUB_PR_TOKEN }} run: | if [ -z "${GH_TOKEN}" ]; then echo "::error::FLATHUB_PR_TOKEN is required when FLATHUB_FORK_REPO is configured." exit 1 fi echo "ready=true" >> "$GITHUB_OUTPUT" - name: Download Flatpak manifest artifact if: ${{ steps.bootstrap.outputs.ready == 'true' }} uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: flatpak-manifests path: dist/flatpak - name: Prepare Flathub submission branch or update PR if: ${{ steps.bootstrap.outputs.ready == 'true' }} env: GH_TOKEN: ${{ secrets.FLATHUB_PR_TOKEN }} FLATHUB_FORK_REPO: ${{ vars.FLATHUB_FORK_REPO }} FLATHUB_TARGET_REPO: ${{ vars.FLATHUB_TARGET_REPO || 'flathub/flathub' }} FLATHUB_BASE_BRANCH: ${{ vars.FLATHUB_BASE_BRANCH || 'new-pr' }} VERSION: ${{ needs.prepare-release.outputs.version }} run: | set -euo pipefail app_id="io.github.thedavidweng.OpenKara" target_repo="${FLATHUB_TARGET_REPO}" base_branch="${FLATHUB_BASE_BRANCH}" fork_owner="${FLATHUB_FORK_REPO%%/*}" submission_mode="false" if [ "${target_repo}" = "flathub/flathub" ] && [ "${base_branch}" = "new-pr" ]; then submission_mode="true" branch="openkara-submission-${VERSION}" pr_title="Add ${app_id}" else branch="openkara/${VERSION}" pr_title="Update OpenKara ${VERSION}" fi git clone "https://x-access-token:${GH_TOKEN}@github.com/${FLATHUB_FORK_REPO}.git" flathub-fork cd flathub-fork git remote add upstream "https://github.com/${target_repo}.git" git fetch upstream "${base_branch}" git checkout -B "${branch}" "upstream/${base_branch}" cp -R "../dist/flatpak/${app_id}/." . git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add . if git diff --cached --quiet; then echo "No Flathub manifest changes to submit" exit 0 fi git commit -m "${pr_title}" git push origin "${branch}" --force pr_url="$(gh pr list --repo "${target_repo}" --head "${fork_owner}:${branch}" --json url --jq '.[0].url')" if [ -n "${pr_url}" ]; then echo "Existing Flathub PR: ${pr_url}" exit 0 fi if [ "${submission_mode}" = "true" ]; then manual_submission_url="https://github.com/${target_repo}/compare/${base_branch}...${fork_owner}:${branch}?expand=1" echo "Initial Flathub submissions must be opened or updated manually: ${manual_submission_url}" exit 0 fi gh pr create \ --repo "${target_repo}" \ --base "${base_branch}" \ --head "${fork_owner}:${branch}" \ --title "${pr_title}" \ --body "Flatpak manifest update for OpenKara ${VERSION}."