Release #58
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: | |
| push: | |
| tags: | |
| - 'v*' | |
| # Manual fallback for when the tag was created by release-please | |
| # using the default GITHUB_TOKEN -- GitHub deliberately does not | |
| # fire workflows on refs authored by GITHUB_TOKEN (loop guard), so | |
| # the push:tags trigger above never runs for those tags. From the | |
| # command line: `task release-fire TAG=vX.Y.Z` (wraps the gh CLI), | |
| # or from the UI: Release -> Run workflow -> pick the tag in the | |
| # "Use workflow from" dropdown. Every job in this file already | |
| # keys off github.ref_name, which is the tag name for both | |
| # triggers. No inputs declared, so nothing untrusted enters the | |
| # workflow context. See RELEASES.md "When release.yml does not | |
| # auto-fire" for the long-term fix. | |
| workflow_dispatch: | |
| permissions: {} # deny all by default; each job declares only what it needs | |
| # Improvement #4 (audit): serialise concurrent runs against the same tag. | |
| # Two ways the same tag could fire this workflow concurrently: (a) push | |
| # trigger races with workflow_dispatch when a maintainer fires it manually | |
| # after the auto-trigger; (b) re-runs from the UI. cancel-in-progress is | |
| # false because a release run is non-idempotent — softprops creates the | |
| # release once, then races with itself if cancelled mid-upload. | |
| concurrency: | |
| group: release-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| # ── Job 1: Cross-platform builds, matrix-parallel ── | |
| # Improvement #1: each of the four Docker-driven targets that used to | |
| # run sequentially inside one `build-docker` job now runs on its own | |
| # runner. Wall-clock drops from ~5m40s (sum) to ~max(individual_build). | |
| # | |
| # Improvement #2: every matrix entry reads/writes the same GHA cache | |
| # scope (`release-build`), so the shared `go-base` / `server-base` | |
| # Dockerfile stages (with their cached `go mod download` and apt | |
| # installs) are materialised once across release runs. The | |
| # `publish-container` job below uses the same scope, so its container- | |
| # image build inherits the layer cache from the binary builds. | |
| build: | |
| strategy: | |
| # Don't cancel the other four targets if one fails — easier to | |
| # diagnose a per-target regression with everything else still in | |
| # the artifact set. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: wails-linux | |
| dockerfile-target: linux-export | |
| output-dir: dist/linux | |
| package-script: package-wails-linux.sh | |
| artifact-glob: | | |
| recall-*-linux-amd64.tar.gz | |
| recall-*-linux-amd64.deb | |
| - target: server-linux | |
| dockerfile-target: server-linux-export | |
| output-dir: dist/server-linux | |
| package-script: package-server-linux.sh | |
| artifact-glob: | | |
| recall-server-*-linux-amd64.tar.gz | |
| recall-server-*-linux-amd64.deb | |
| - target: server-windows | |
| dockerfile-target: server-windows-export | |
| output-dir: dist/server-windows | |
| package-script: package-server-windows.sh | |
| artifact-glob: | | |
| recall-server-*-windows-amd64.exe | |
| - target: server-mac | |
| dockerfile-target: server-mac-export | |
| output-dir: dist/server-mac | |
| package-script: package-server-mac.sh | |
| artifact-glob: | | |
| recall-server-*-darwin-arm64.tar.gz | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write # needed by Sigstore to mint a short-lived cert | |
| attestations: write # needed to write SLSA provenance to the repo | |
| env: | |
| DOCKER_BUILDKIT: "1" | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: ./.github/actions/docker-build-env | |
| # docker/build-push-action handles `--cache-from` / `--cache-to` | |
| # natively (the Taskfile's `docker build` invocation does not), | |
| # which is why this step bypasses `task ${{ matrix.target }}` | |
| # and calls the Dockerfile target directly. Same outputs, same | |
| # build-args — local `task build-*` remains the un-changed | |
| # canonical local-build entry point. | |
| - name: Build ${{ matrix.target }} | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 | |
| with: | |
| context: . | |
| file: Dockerfile.build | |
| target: ${{ matrix.dockerfile-target }} | |
| outputs: type=local,dest=${{ matrix.output-dir }} | |
| build-args: | | |
| VERSION=${{ github.ref_name }} | |
| cache-from: type=gha,scope=release-build | |
| cache-to: type=gha,mode=max,scope=release-build | |
| - name: Package ${{ matrix.target }} | |
| env: | |
| VERSION: ${{ github.ref_name }} | |
| run: bash scripts/release/${{ matrix.package-script }} | |
| # SLSA provenance per matrix entry. Each entry's artifact set is | |
| # bounded by its package script's outputs; reusing one glob across | |
| # all entries would over-claim (e.g. wails-linux claiming the | |
| # server-mac tarball). Subject-paths below mirror each script's | |
| # documented outputs. | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4 | |
| with: | |
| subject-path: | | |
| recall-*-linux-amd64.tar.gz | |
| recall-*-linux-amd64.deb | |
| recall-server-*-linux-amd64.tar.gz | |
| recall-server-*-linux-amd64.deb | |
| recall-server-*-windows-amd64.exe | |
| recall-server-*-darwin-arm64.tar.gz | |
| - name: Upload ${{ matrix.target }} artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: build-${{ matrix.target }} | |
| path: | | |
| recall-*-linux-amd64.tar.gz | |
| recall-*-linux-amd64.deb | |
| recall-server-*-linux-amd64.tar.gz | |
| recall-server-*-linux-amd64.deb | |
| recall-server-*-windows-amd64.exe | |
| recall-server-*-darwin-arm64.tar.gz | |
| if-no-files-found: ignore | |
| # ── Job 2: macOS Wails .app bundles (requires Apple SDK / Xcode CLT) ── | |
| # Stays a single job (not in the matrix above) because it MUST run on | |
| # macos-latest — the Apple SDK isn't redistributable, so cross-build | |
| # from Linux is impossible. | |
| build-mac: | |
| runs-on: macos-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: ./.github/actions/wails-build-env | |
| - name: Build macOS Wails app | |
| run: task build-mac | |
| # DMG staging + hdiutil call lives in scripts/release/make-dmg.sh. | |
| # The in-DMG README.txt is sourced from docs/dmg/README.txt | |
| # (single-source-of-truth instead of the previous heredoc paired | |
| # with docs/install-macos.md sections 2-3). | |
| - name: Create DMG | |
| env: | |
| VERSION: ${{ github.ref_name }} | |
| run: bash scripts/release/make-dmg.sh | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4 | |
| with: | |
| subject-path: recall-*-darwin-arm64.dmg | |
| - name: Upload macOS artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: build-mac | |
| path: | | |
| recall-*-darwin-arm64.dmg | |
| # ── Job 3: Windows app + NSIS installer (native cross-compile, no Docker) ── | |
| # v3's WebView2 loader is pure Go, so this cross-compiles CGO_ENABLED=0 from | |
| # Linux — no mingw, no Docker. wails3 generate syso embeds the icon/version; | |
| # create:nsis:installer fetches the WebView2 bootstrapper + runs makensis. | |
| build-windows: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # Building the wails3 CLI itself (mise/go install) pulls in v3's CGo GTK4 | |
| # webview, so the Ubuntu runner needs the WebKit dev libs — even though the | |
| # Windows target is CGO_ENABLED=0. | |
| - uses: ./.github/actions/linux-webview-deps | |
| - uses: ./.github/actions/wails-build-env | |
| - name: Install makensis | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends nsis | |
| - name: Build Windows app + NSIS installer | |
| run: task build-windows | |
| - name: Package | |
| env: | |
| VERSION: ${{ github.ref_name }} | |
| run: bash scripts/release/package-wails-windows.sh | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4 | |
| with: | |
| subject-path: recall-*-windows-amd64-installer.exe | |
| - name: Upload Windows artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: build-wails-windows | |
| path: | | |
| recall-*-windows-amd64-installer.exe | |
| # ── Job 4: Generate SBOM from BUILT ARTIFACTS ──────────────────────── | |
| # Improvement #3 (audit): SBOM now describes the shipped bits, not | |
| # just the source tree. Downloads every build artifact, untars the | |
| # tarballs so syft can scan the actual Go binaries (catches build-info | |
| # entries the source-tree scan would miss — indirect deps that vendor | |
| # brought in, plus any compile-time-resolved version pins). The repo | |
| # checkout is still in the scan path so syft's go.mod / package.json | |
| # scanners run too — the resulting SBOM is the union of both passes. | |
| sbom: | |
| needs: [build, build-mac] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Extract tarballs (so syft scans their binaries) | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts-extracted | |
| for tar in artifacts/*.tar.gz; do | |
| [ -e "$tar" ] || continue | |
| tar -xzf "$tar" -C artifacts-extracted/ | |
| done | |
| - name: Compute artifact version | |
| id: ver | |
| run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" | |
| - name: Generate SBOM (SPDX JSON) | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 | |
| with: | |
| # Scans repo source AND the extracted built binaries that | |
| # were downloaded into the workspace. | |
| path: . | |
| format: spdx-json | |
| output-file: recall-${{ steps.ver.outputs.version }}-sbom.spdx.json | |
| upload-artifact: false | |
| upload-release-assets: false | |
| - name: Upload SBOM artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: sbom | |
| path: recall-${{ steps.ver.outputs.version }}-sbom.spdx.json | |
| # ── Job 5: Create GitHub Release with all binaries attached ── | |
| # github.ref_name is used only in an `actions/` `with:` expression, not | |
| # interpolated into any run: shell command — safe from injection. | |
| release: | |
| needs: [build, build-mac, build-windows, sbom] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write # needed by Sigstore to mint a short-lived cert | |
| attestations: write # needed to write SLSA provenance for sha256 files | |
| steps: | |
| # Checkout is required even though this job only consumes artifacts | |
| # from earlier jobs — the "Generate SHA256 checksums" step shells | |
| # out to scripts/release/compute-sha256.sh, which lives in the | |
| # source tree. Without checkout the script is absent from the | |
| # runner filesystem and the step dies with exit 127. | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # Download all artifacts into the current directory (merge-multiple flattens them). | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| merge-multiple: true | |
| # Stage the parser's reference data (heroes.yaml + maps.yaml) as | |
| # release-versioned assets. The frontend's "verify reference data" | |
| # affordance (next feature) downloads these to compare the local | |
| # parser corpus against the canonical version. Renaming with the | |
| # tag in the filename keeps each release's data self-describing | |
| # and prevents an old asset from being mistaken for a newer one | |
| # if a verifier downloads from the wrong release page. | |
| - name: Compute artifact version | |
| id: ver | |
| run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" | |
| - name: Stage reference data YAMLs | |
| env: | |
| # Route the tag through an env var rather than interpolating | |
| # ${{ … }} directly into the run: shell — project convention | |
| # (see .claude/rules/ci-cd.md "Fixing CI on a remote-authored | |
| # PR" / build-tooling notes). VERSION is bounded by git's | |
| # ref-naming rules + the v* push trigger, so there's no | |
| # injection vector today, but the env-var shape keeps the | |
| # workflow safe against a future change to the trigger. | |
| VERSION: ${{ steps.ver.outputs.version }} | |
| run: | | |
| cp pkg/parser/heroes.yaml "recall-${VERSION}-heroes.yaml" | |
| cp pkg/parser/maps.yaml "recall-${VERSION}-maps.yaml" | |
| cp pkg/parser/screenshot_sources.yaml "recall-${VERSION}-screenshot_sources.yaml" | |
| # The Windows DB-reset helper (also bundled in the installer). A | |
| # checked-in source file copied straight into the workspace, like | |
| # the reference YAMLs — so it's signed + attested the same way. | |
| - name: Stage Windows reset-DB helper script | |
| env: | |
| VERSION: ${{ steps.ver.outputs.version }} | |
| run: | | |
| cp scripts/windows/Reset-Database.bat "recall-${VERSION}-Reset-Database.bat" | |
| # SLSA build provenance on the reference-data YAMLs. The binary | |
| # build jobs attested their own outputs in-job; the YAMLs aren't | |
| # built by any earlier job (they're checked-in source copied | |
| # straight into the workspace), so the attestation has to fire | |
| # here in the release job. Verify with: | |
| # gh attestation verify recall-X.Y.Z-heroes.yaml \ | |
| # --repo sound-barrier/recall | |
| - name: Attest reference data provenance | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4 | |
| with: | |
| subject-path: | | |
| recall-*-heroes.yaml | |
| recall-*-maps.yaml | |
| recall-*-screenshot_sources.yaml | |
| recall-*-Reset-Database.bat | |
| # Per-artifact .sha256 files for every release binary, package, | |
| # and reference-data YAML. Each file contains the SHA256 hash of | |
| # exactly one artifact. | |
| # Users verify with: sha256sum --check recall-<version>-linux-amd64.tar.gz.sha256 | |
| # On macOS: shasum -a 256 --check recall-<version>-darwin-arm64.dmg.sha256 | |
| - name: Generate SHA256 checksums | |
| run: bash scripts/release/compute-sha256.sh | |
| # Attest the sha256 files so users can prove the checksums themselves | |
| # came from CI, not a tampered release page. Chain: | |
| # gh attestation verify <file>.sha256 → sha256sum --check <file>.sha256 | |
| # This closes the loop: even the fallback verification path is signed. | |
| - name: Attest SHA256 checksums | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4 | |
| with: | |
| subject-path: | | |
| recall-*.tar.gz.sha256 | |
| recall-*.deb.sha256 | |
| recall-*.exe.sha256 | |
| recall-*.dmg.sha256 | |
| recall-*.yaml.sha256 | |
| recall-*.bat.sha256 | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 | |
| with: | |
| # Tags like v0.0.1-beta or v1.0.0-rc1 contain a hyphen → prerelease. | |
| prerelease: ${{ contains(github.ref_name, '-') }} | |
| generate_release_notes: true | |
| files: | | |
| recall-*.tar.gz | |
| recall-*.tar.gz.sha256 | |
| recall-*.deb | |
| recall-*.deb.sha256 | |
| recall-*.exe | |
| recall-*.exe.sha256 | |
| recall-*.dmg | |
| recall-*.dmg.sha256 | |
| recall-*.yaml | |
| recall-*.yaml.sha256 | |
| recall-*.bat | |
| recall-*.bat.sha256 | |
| recall-*-sbom.spdx.json | |
| # ── Job 6: Build and push Linux server container image to GHCR ── | |
| # Improvement #5 (audit): now depends on `release`. Previously this | |
| # ran in parallel with the binary builds, sometimes producing a | |
| # container image even when the actual GitHub Release failed | |
| # (e.g. v0.2.0 / v0.2.1 had container images on GHCR but no usable | |
| # release assets). Gating on `release` means GHCR never has a tag | |
| # users can `docker pull` unless the matching downloadable release | |
| # exists. | |
| # | |
| # Uses GITHUB_TOKEN (auto-injected, repo-scoped, expires after run — no PAT needed). | |
| # Image: ghcr.io/<owner>/recall-server:<version> (always), plus :<major>.<minor> | |
| # and :latest on stable releases only (prerelease tags skip both — see the | |
| # `enable=` guards in the metadata step below). | |
| publish-container: | |
| needs: [release] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write # required by cosign for keyless OIDC signing | |
| env: | |
| DOCKER_BUILDKIT: "1" | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: ./.github/actions/docker-build-env | |
| with: | |
| ghcr-login: 'true' | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract image metadata | |
| id: meta | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 | |
| with: | |
| images: ghcr.io/${{ github.repository_owner }}/recall-server | |
| # Stable releases get the exact :{{version}} plus rolling :{{major}}.{{minor}} | |
| # and :latest tags. Prerelease tags (those containing a hyphen, e.g. | |
| # v0.1.0-beta.0) MUST get ONLY :{{version}} — the rolling tags are | |
| # supposed to track the most-recent stable, so a `docker pull | |
| # recall-server:latest` always lands on a non-prerelease build. | |
| tags: | | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref_name, '-') }} | |
| type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }} | |
| - name: Build and push server container | |
| id: push | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 | |
| with: | |
| context: . | |
| file: Dockerfile.build | |
| target: server-container | |
| platforms: linux/amd64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| # Improvement #2: same cache scope as the `build` matrix | |
| # above, so the `go-base` / `server-base` layers materialised | |
| # during the binary builds are reused here. Materially | |
| # faster than the old standalone cache (~2m saved). | |
| cache-from: type=gha,scope=release-build | |
| cache-to: type=gha,mode=max,scope=release-build | |
| # Sign each pushed tag with cosign keyless (Sigstore OIDC). The | |
| # workflow's own GitHub Actions identity is the signing identity; | |
| # users verify with: | |
| # cosign verify ghcr.io/sound-barrier/recall-server:<tag> \ | |
| # --certificate-identity-regexp 'https://github.com/sound-barrier/recall/\.github/workflows/release\.yml@refs/tags/v.*' \ | |
| # --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' | |
| # See docs/docker.md for the full verification recipe. | |
| - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| with: | |
| cosign-release: 'v2.4.1' | |
| - name: Sign container image (keyless via OIDC) | |
| env: | |
| DIGEST: ${{ steps.push.outputs.digest }} | |
| TAGS: ${{ steps.meta.outputs.tags }} | |
| run: bash scripts/release/sign-image.sh | |
| # GHCR packages default to private for new packages. Try to set | |
| # public so users can pull without auth — script handles the | |
| # org-vs-user owner-type branching + retries. continue-on-error | |
| # because GITHUB_TOKEN lacks write:packages OAuth scope for | |
| # visibility changes; the package must be set public once | |
| # manually via GitHub Package settings. | |
| - name: Set container image visibility to public | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: bash scripts/release/flip-package-public.sh |