diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a5356a8..359c4f23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,19 @@ on: 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 @@ -66,12 +79,35 @@ jobs: - name: Install syft (for SBOM generation) uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0 + # 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@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: - version: "~> v2" - args: release --clean + 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 }} # Cross-repo PAT for pushing the brew formula to PerpetualSoftware/homebrew-tap. @@ -79,6 +115,17 @@ jobs: # 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 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 34d4c21b..39253e65 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,11 +1,17 @@ version: 2 -before: - hooks: - # Build web UI before Go compilation so it gets embedded. - # v2 accepts simple string hooks only at this level; object-with-cmd - # form (v1 syntax) is rejected with "cannot unmarshal !!map into string". - - bash -c "cd web && npm ci && npm run build" +# NOTE: the SvelteKit web UI is built by a dedicated workflow step in +# .github/workflows/release.yml *before* the goreleaser-action runs, NOT +# by a `before:` hook here. Done deliberately so the npm install/build +# does not inherit the macOS signing secrets (Developer ID .p12, .p8 notary +# key, etc.) that are scoped only to the `Run GoReleaser` step's env. +# Reduces blast radius from any npm supply-chain compromise. +# +# Local testing (`goreleaser release --snapshot` or similar): run +# `make web` first (or `cd web && npm ci && npm run build`) so `web/build/` +# exists — the Go binary embeds it via `//go:embed all:web/build`, so a +# clean checkout will fail with "embed: no matching files found" otherwise. +# `make build` / `make install` handle the web build automatically. builds: - main: ./cmd/pad @@ -62,6 +68,25 @@ signs: artifacts: checksum output: true +# macOS code-signing + Apple notarization. Gated on MACOS_CERT_P12 being set +# so PR builds and snapshot mode don't fail when the cert isn't available +# (e.g. forks, local `goreleaser release --snapshot`). Uses GoReleaser v2's +# built-in Anchore/Quill backend — no external tools required on the runner. +# The .p12 cert and .p8 notary key are both supplied as base64-encoded +# strings via repo secrets; Quill decodes them in-process. +notarize: + macos: + - enabled: '{{ isEnvSet "MACOS_CERT_P12" }}' + sign: + certificate: "{{ .Env.MACOS_CERT_P12 }}" + password: "{{ .Env.MACOS_CERT_PASSWORD }}" + notarize: + issuer_id: "{{ .Env.MACOS_NOTARY_ISSUER_ID }}" + key_id: "{{ .Env.MACOS_NOTARY_KEY_ID }}" + key: "{{ .Env.MACOS_NOTARY_KEY_P8 }}" + wait: true + timeout: 20m + # Keyless cosign signing for every container image manifest. docker_signs: - cmd: cosign