name: Release on: push: tags: - "v*" # Serialize all release runs. If two v* tags land close together (e.g. # rc.3 then rc.4 within a minute), queue rather than race — they share # mutable outputs (the GHCR `:latest` tag, the homebrew cask in the # separate tap repo, the GitHub Releases page) and parallel runs would # interleave nondeterministically. Group is intentionally NOT keyed by # `github.ref`: we want different tag names to serialize too, not just # repeat pushes of the same tag. cancel-in-progress=false so a queued # tag never aborts a release mid-publish (which could leave GHCR and the # brew tap in inconsistent states). concurrency: group: release cancel-in-progress: false permissions: contents: write packages: write # id-token: write is required for keyless cosign signing (GitHub OIDC # exchanges this workflow's identity token for a short-lived Fulcio # certificate) and for actions/attest-build-provenance to mint SLSA # v1 provenance statements. id-token: write # attestations: write is required by actions/attest-build-provenance so # the resulting provenance bundles can be stored against the repo. attestations: write # All third-party Actions are pinned to a 40-char commit SHA with a trailing # '# vX.Y.Z' comment so a compromised maintainer or moved tag cannot silently # execute attacker code in the release pipeline (this workflow has # contents:write + packages:write + the GHCR token, so a malicious action # here could publish tampered binaries). Bump the SHA + comment together. jobs: release: name: Build & Release runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "1.26" - name: Allow Go to fetch the toolchain go.mod pins # actions/setup-go pins GOTOOLCHAIN=local, which blocks the toolchain # go.mod requires (`go 1.26.5` floor, `toolchain go1.26.6`) from being # fetched. `auto`, written after setup-go so it wins the $GITHUB_ENV # last-write, lets Go pull it on demand. Mirrors the CI workflow; see # #896. As of BUG-2565 this also decides which stdlib the RELEASED # binaries carry — under `local` they would ship the 1.26.5 stdlib and # its 8 reachable advisories. run: echo "GOTOOLCHAIN=auto" >> "$GITHUB_ENV" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "24" cache: "npm" cache-dependency-path: web/package-lock.json - name: Create web build placeholder for tests run: mkdir -p web/build && echo "placeholder" > web/build/.gitkeep - name: Run tests # Explicit -timeout, same reasoning as ci.yml's steps (TASK-2545): # `go test` defaults to 10m per test binary, and this is the RELEASE # gate — the one place an unchosen default is most expensive. SQLite # only here (no PAD_TEST_POSTGRES_URL), so it is the faster driver, # but it grows on the same curve. run: go test -timeout=45m ./... - name: Set up Docker Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Login to GitHub Container Registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # cosign + syft need to be on PATH before goreleaser runs — goreleaser # shells out to both for the signs/docker_signs/sboms sections. - name: Install cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Install syft (for SBOM generation) uses: anchore/sbom-action/download-syft@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 # Build the SvelteKit web UI before GoReleaser so the static assets # get embedded into the Go binary. Done as a dedicated step (instead # of a goreleaser `before:` hook) so the npm install/build does NOT # inherit the MACOS_* signing secrets — those are scoped only to the # `Run GoReleaser` step's env block below. This isolates the 5-year # Developer ID cert from any npm supply-chain compromise during # dependency install. - name: Build web UI run: cd web && npm ci && npm run build - name: Run GoReleaser id: goreleaser # GoReleaser binary is pinned to an exact version (not "~> v2") to # match the SHA-pinning policy applied to the Actions themselves — # see the comment at the top of this file. With Apple signing # credentials now flowing through this step, a compromised or # regressed GoReleaser release would carry meaningful blast radius; # pinning forces an explicit, reviewed bump. uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: version: "v2.15.4" # --timeout=2h overrides GoReleaser's 1h default. With Apple # notarization (`wait: true`, up to 20m per the .goreleaser.yaml # notarize block) layered on top of build + cosign blob-sign + # SBOM + multi-arch docker manifest, slow notary days could push # close to the default ceiling. 2h gives comfortable headroom # without burning excessive Action minutes when notarization # actually fails fast (the worker exits as soon as Apple replies). args: release --clean --timeout=2h env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Force-set the release tag from the triggering ref to bypass # goreleaser's git-describe-based auto-detection. When two # lightweight tags point at the same commit (e.g. v0.4.0 cut # right on top of v0.4.0-rc.1 with no intervening commits), # git-describe's tiebreaker is non-deterministic across hosts # — locally it picked v0.4.0, the CI runner picked v0.4.0-rc.1 # during the v0.4.0 ship and stamped artifacts with the RC # version. github.ref_name is unambiguous: it's exactly the # tag that triggered the workflow. See PLAYB-1160 failure modes. GORELEASER_CURRENT_TAG: ${{ github.ref_name }} # Cross-repo PAT for pushing the brew formula to PerpetualSoftware/homebrew-tap. # The default GITHUB_TOKEN above only has access to PerpetualSoftware/pad. # Add this secret in repo settings before tagging a release that ships # a brew formula — without it goreleaser fails at the brew publish step. HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} # macOS code-signing + Apple notarization (per IDEA-830). The # `notarize:` block in .goreleaser.yaml is gated on MACOS_CERT_P12 # being set, so PR builds + snapshot mode skip cleanly when these # are absent. The .p12 cert and .p8 notary key are stored # base64-encoded; GoReleaser's Quill backend decodes them in-process, # so no external signing tool needs to be installed on the runner. MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }} MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} MACOS_NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_KEY_P8 }} MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} # SLSA build provenance for every archive GoReleaser produced. # Writes a Sigstore-backed attestation to the repo so downstream # consumers can verify this binary was actually built by this # workflow from this commit, e.g.: # gh attestation verify pad_v1.0.0_linux_amd64.tar.gz \ # --repo PerpetualSoftware/pad - name: Generate build provenance for archives uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: "dist/pad_*_*_*.tar.gz,dist/pad_*_*_*.zip"