Skip to content

8. GoReleaser Release Pipeline, SHA-Pinned Actions, and Signed Artifacts

Status

Accepted

Context

The release pipeline was hand-rolled. make release shelled out to a for loop of go build invocations for each GOOS/GOARCH, cyclonedx-gomod produced the SBOM, a separate workflow job built and pushed the container image with docker/build-push-action, and softprops/action-gh-release published the GitHub Release. The SBOM was generated by a different tool (cyclonedx-gomod) than the image SBOM (buildx/syft), so there were two SBOM definitions to keep in sync. Adding artifact signing or a Homebrew formula would have meant bolting yet more bespoke steps onto the Makefile and workflow.

Separately, every workflow referenced its GitHub Actions by mutable tag (actions/checkout@v6, docker/login-action@v4, …). A mutable tag can be moved to point at malicious code after review, so a tag-pinned workflow trusts every future force-push to that tag — a well-known CI/CD supply-chain weakness.

Decision

Adopt GoReleaser (.goreleaser.yaml, schema v2) as the single release engine, and pin all Actions to commit SHAs.

  • One pipeline builds the binaries, .tar.gz archives, and checksums.txt; generates a CycloneDX SBOM per archive via syft (replacing cyclonedx-gomod, making syft the single SBOM source); builds the multi-arch ghcr.io/fjacquet/pstore_exporter image via the dockers_v2 (buildx) pipe with sbom: true for SBOM + provenance attestations; signs checksums.txt with keyless cosign (Sigstore bundle) over GitHub OIDC; and publishes a Homebrew cask to fjacquet/homebrew-tap.
  • A dedicated Dockerfile.goreleaser copies the pre-built per-platform binary (${TARGETPLATFORM}/pstore_exporter) instead of recompiling; the multi-stage ./Dockerfile is retained for local/dev and Docker Compose builds.
  • All GitHub Actions are pinned to full commit SHAs with a # vX comment in ci.yml, release.yml, and docs.yml. The semgrep/semgrep scanner container is deliberately left on a rolling tag so it runs current rules.
  • CI regenerates the SBOMs in snapshot mode (make sbomgoreleaser release --snapshot --skip=publish,sign,docker,homebrew) so the PR-time SBOM uses the exact same definition as a real release.

Consequences

  • One declarative config replaces the cross-compile loop, the separate SBOM step, and the separate image job. Release assets are now .tar.gz archives rather than raw binaries.
  • The release SBOM and the image SBOM both come from syft — a single source of truth. make tools no longer installs cyclonedx-gomod.
  • Artifacts are verifiable: cosign verify-blob --bundle checksums.txt.sigstore.json checksums.txt, then sha256sum -c checksums.txt.
  • New required secret: HOMEBREW_TAP_GITHUB_TOKEN (a PAT with contents:write on the tap repo; the default GITHUB_TOKEN cannot push to another repository). The release job also needs packages: write and id-token: write permissions.
  • Pinned SHAs must be bumped deliberately (e.g. Dependabot/Renovate keyed on the # vX comment) rather than drifting with upstream tags — the intended trade-off of supply-chain integrity over automatic updates.
  • Local release tooling (goreleaser, syft, cosign) is now a prerequisite for make sbom/make release; install via brew install goreleaser syft cosign. CI provides them through pinned actions.