ADR-0001: GoReleaser release pipeline, SHA-pinned actions, and supply-chain hardening
- Status: Accepted
- Date: 2026-06-05
- Deciders: Frederic Jacquet
Context
The release path had grown organically and carried three weaknesses:
- Bespoke release tooling.
make releasecross-compiled binaries with a hand-written shell loop andsha256sum, thensoftprops/action-gh-releaseuploaded them. A separatedocker/build-push-actionjob built the GHCR image. SBOMs came fromcyclonedx-gomod. Three tools, two jobs, two SBOM formats, and behaviour that only existed in CI — hard to reproduce locally and easy to drift. - Mutable action tags. Every GitHub Action was referenced by a moving tag
(
actions/checkout@v6,docker/build-push-action@v7, …). A compromised or retagged action would execute with ourcontents: write/packages: write/id-token: writepermissions — a well-known supply-chain vector. - No macOS distribution channel beyond raw binaries, and a
LICENSEthat the README advertised but that did not exist in the repo.
Decision
Adopt GoReleaser (v2, dockers_v2) as the single release engine and harden the
supply chain:
- One config, one tool.
.goreleaser.yamlowns binaries,tar.gzarchives,checksums.txt, per-archive CycloneDX SBOMs (syft), and the multi-arch (linux/amd64,linux/arm64) GHCR image. The image keeps its SBOM + provenance attestations — now produced bydockers_v2, which enables both by default. - Pin every GitHub Action to a full commit SHA, annotated with a
# vX.Y.Zcomment, acrossci.yml,release.yml, anddocs.yml. Pin the Semgrep CI container by image digest. Dependabot can still raise version-bump PRs against the pinned SHAs. - Distribute via a Homebrew cask in
github.com/fjacquet/homebrew-tap(brew install fjacquet/tap/pscale_exporter). A cross-repo push needs a dedicatedHOMEBREW_TAP_GITHUB_TOKENPAT, since the workflow'sGITHUB_TOKENcannot write to another repository. - Add the missing Apache-2.0
LICENSEand bundle it into release archives. - Bump the Go builder image to 1.26.4 to match the
go.moddirective.
A dedicated runtime Dockerfile.goreleaser (no Go builder stage — GoReleaser stages the
prebuilt binary) is used for images; the root Dockerfile remains the from-source build for
make docker. Both declare a non-root USER, per the project's Semgrep constraint.
Alternatives considered
- Keep the bespoke scripts, add SHA-pinning only. Rejected: leaves the three-tool/two-job sprawl and the local/CI drift untouched.
- GoReleaser
dockers+docker_manifests(the older builder). Rejected in favour ofdockers_v2, which expresses multi-arch in one block and gives image SBOM/provenance for free, removing the per-arch boilerplate. - Homebrew formula (
brews:). Rejected because GoReleaser has deprecatedbrewsin favour ofhomebrew_casksfor binary distribution. - syft for the CI SBOM too. Deferred: the
ci.ymlSBOM job still usescyclonedx-gomodfor a fast module-level SBOM on PRs; release artifacts use syft. Revisit if the dual tooling becomes a maintenance burden.
Consequences
Positive
- Releases reproduce locally via
make release-snapshot(full pipeline minus publish). - Pinned SHAs mean a known, immutable set of third-party code runs with our write tokens.
- One artifact story: binaries, archives, checksums, SBOMs, image, and cask from one
git tag. - macOS users get
brew install.
Negative / costs
- A new
HOMEBREW_TAP_GITHUB_TOKENsecret and ahomebrew-taprepo must exist before the cask step succeeds. - SHA pins are opaque to read; the
# vX.Y.Zcomments and Dependabot mitigate this. - The release job now needs QEMU + Buildx + syft available (handled by pinned setup actions).
- Two SBOM tools remain in play (cyclonedx-gomod on PRs, syft on releases).
References
- Keep a Changelog —
CHANGELOG.md - GoReleaser
dockers_v2,homebrew_casks, andsbomscustomization docs - GitHub: "Using third-party actions" — pin actions to a full-length commit SHA