fix(release): brew tap out of the release critical path so SLSA provenance ships (closes #1763, #1760) - #1767
Conversation
…rovenance ships goreleaser pushes the homebrew formula from inside `goreleaser release`, so a tap failure fails the whole invocation. With HOMEBREW_TAP_GITHUB_TOKEN returning 401 Bad credentials, every release died at the GoReleaser step and every step after it was skipped — including "Attest build provenance". v1.10.9, v1.10.10 and v1.10.11 therefore shipped with no SLSA provenance while SECURITY.md tells users to run `gh attestation verify`. The attestation steps were never disabled; they were unreachable. - .goreleaser.yml: brews.skip_upload: true, so goreleaser only generates dist/homebrew/Formula/agent-deck.rb and never talks to the tap. Drops the repository.token template with it, which also frees `make release-local` and release-snapshot.yml from needing the tap secret at all. - release.yml: sign provenance BEFORE publishing, so an unattested release is never made visible (the attest step only reads local files, so it does not need a public release). - release.yml: new final "Update Homebrew tap formula" step with continue-on-error, after publish (the formula's URLs only resolve once the release is public). It refuses to push unless the formula's version matches the tag and all four sha256 values match the verified checksums.txt, guards against an empty encode, and re-reads the tap through the API afterwards so a silent no-op cannot pass as success. Failures emit a ::warning:: plus a job summary with recovery steps instead of failing the release. - Docs: workflows README on the new tail ordering, SECURITY.md states the v1.10.9/10/11 attestation gap honestly, Makefile drops the stale tap-token pre-flight. Stays on the deprecated `brews` config on purpose: casks are macOS-only and would drop Linuxbrew users, the migration needs tap-side changes that cannot be validated while the token is 401, and goreleaser removes deprecated options only on majors while the action is pinned to `~> v2`. Rationale and a migration checklist are recorded above the brews block. Closes #1763 Closes #1760 Refs #1761 Committed by Ashesh Goplani
|
Warning Review limit reached
Next review available in: 45 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
👋 Thanks for opening this — a couple of intake fields would help the maintainer's validation pipeline (and any reviewer) understand it faster. Nothing here blocks you and nothing is closing: this is a heads-up, and editing the description re-runs this check automatically.
The full field-level contract is in .github/INTAKE.md. AI-authored PRs are welcome here with equal standing. |
Closes #1763
Closes #1760
Refs #1761 (documented decision — see "The deprecated
brewsconfig" below; needs a human call before that issue is closed)Completes the draft-first release work from #1762: the Homebrew tap comes out of the release's critical path, and SLSA build provenance ships again.
The bug, precisely
goreleaser releasepushes the brew formula from inside the release run, and a tap failure fails the whole invocation. The tap token has been returning401 Bad credentials, so every release died at the GoReleaser step — and everything after it was skipped:From the v1.10.11 run (
30216679269):Step outcomes for that same run:
So the attestation steps were never disabled — they were unreachable. A best-effort package manager was able to cancel build provenance for three consecutive releases (v1.10.9, v1.10.10, v1.10.11) while
SECURITY.mdtells users to rungh attestation verify.The fix
1.
.goreleaser.yml—brews.skip_upload: true. GoReleaser now only generatesdist/homebrew/Formula/agent-deck.rband never talks to the tap, so a dead tap token cannot failgoreleaser release. Therepository.tokentemplate is removed too: withskip_uploadthere is nothing to authenticate, which also meansmake release-localno longer requires the tap secret andrelease-snapshot.ymlno longer needs itsHOMEBREW_TAP_GITHUB_TOKEN: snapshot-placeholderworkaround (that placeholder existed only to keep{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}resolvable).2.
release.yml— provenance is signed BEFORE publishing.Attest build provenancemoved abovePublish release. Beyond un-skipping it, this makes provenance a precondition of visibility: if signing fails, the draft is simply never published and no user is handed an artifact thatgh attestation verifycannot check. The attest step only reads localdist-attest/files, so it does not need the release to be public. If Sigstore is genuinely down and a release must go out anyway, the verified draft can be published by hand (documented in the step comment).3.
release.yml— new finalUpdate Homebrew tap formula (best effort)step,continue-on-error: true, running afterPublish release(required for correctness: the formula's download URLs only resolve once the release is public, andhomebrew-verify.ymlcompares the tap against/releases/latest).It cannot fail the job, but it is not silent either. Every non-success path emits a
::warning::annotation and an actionable job summary (how to rotate the token, how to push by hand). And it refuses to push a bad formula:versionmust equal the tag;sha256values must match the release's already-verifiedchecksums.txt, paired url-by-url — a formula with a stale checksum breaksbrew installfor every user with a checksum error, which is worse than a stale formula;contentis base64-encoded into a variable and checked non-empty first, so a broken encode can never overwrite the tap's formula with an empty file;raw.githubusercontent.com, which is CDN-cached for minutes) and fails if the version did not change — the same "a silent no-op must not pass as success" paranoia asPublish release.4. Docs.
.github/workflows/README.mdgains a section on the new tail ordering.SECURITY.mdstates the known gap honestly: v1.10.9/10/11 have no attestation, that is a broken pipeline rather than a tampered artifact, and a hand checksum check is not a signature.Makefiledrops the now-unnecessaryHOMEBREW_TAP_GITHUB_TOKENpre-flight and says plainly that a local release does not attest and does not touch the tap.The deprecated
brewsconfig (#1761) — decision, not an oversightGoReleaser soft-deprecated
brewsin v2.10 and hard-deprecated it in v2.16 in favour ofhomebrew_casks. This PR deliberately stays onbrews, for four reasons:linux/amd64+linux/arm64, andREADME.md,install.shandhomebrew-verify.ymlall advertisebrew install asheshgoplani/tap/agent-deckto Mac and Linux/WSL users (Missing homebrew formula #873). A cask silently drops every Linuxbrew user.tap_migrations.jsonplus deletingFormula/agent-deck.rbinasheshgoplani/homebrew-tap, so existing installs transition instead of breaking. None of that can be reviewed or reverted from this repo, andscripts/verify-homebrew-install.shis hard-wired toFormula/agent-deck.rb.The deadline risk is bounded and stated in the GoReleaser docs: "Deprecated options are only removed on major versions of GoReleaser", and
release.ymlconstrains the action to~> v2. Confirmed empirically: the v1.10.11 run used goreleaser 2.17.1 andbrewsstill only warns. Sobrewscannot turn into a hard error without an explicit major bump in this repo. Until then the deprecated block does nothing but render a file locally — the least exposed way to use it. The full rationale and a migration checklist are recorded as a comment above thebrewsblock so the next person does not have to rediscover it.#1761 therefore needs a human call. If you accept the reasoning, close it as decided; if you want the cask migration, it should be its own PR after the tap token is rotated and this pipeline change has survived one real release.
Maintainer action still required
HOMEBREW_TAP_GITHUB_TOKENis still dead. After this PR the tap step will show as red-but-ignored on every release until it is rotated — visible, non-blocking, and no longer able to cost the release its provenance. To fix at the root: create a fine-grained PAT with Contents: read/write onasheshgoplani/homebrew-tapand set it as theHOMEBREW_TAP_GITHUB_TOKENrepo secret. (The tap formula is currently at 1.10.11 and correct — it was pushed by hand.)Verification
No Go code changed. The true test is the next real release; everything testable ahead of that was tested.
actionlintclean onrelease.yml,release-snapshot.yml,homebrew-verify.yml.goreleaser checkvalidates.goreleaser.yml(only the expectedbrewsdeprecation notice).goreleaser release --snapshot --cleanend-to-end locally: exit 0, all four targets cross-compiled, and withskip_uploadthe formula is still written —writing formula=dist/homebrew/Formula/agent-deck.rb. This is what confirms the path the new step reads, and that no tap credentials are needed any more.dist/checksums.txt(4/4 match), and the live v1.10.11 tap formula (goreleaser 2.17.1 output) against the published v1.10.11checksums.txt(4/4 match, version parsed as1.10.11).gh: happy path (exit 0, tap updated) plus missing token, no formula indist/, formula/tag version mismatch, checksum mismatch, missingchecksums.txt, tap 401, only 3 url/sha256 pairs, and a "push returned success but the tap still shows the old version" no-op. All eight failure paths exit non-zero with the right message and a job summary; none of them can fail the job.bash -non the step script;make -n release-localparses.Coordination
Touches no Go files and nothing under
internal/ui, so there is no overlap with the in-flight TUI perf work (#1764, #1765). One adjacency: #1687 also editsrelease-snapshot.yml, but in theon.push.pathslist (~line 21) while this PR edits the goreleaser step'senv:(~line 49) — different hunks, no conflict expected.