Skip to content

chore(release): automate the ritual around goreleaser - #4

Merged
dejanstrbac merged 1 commit into
mainfrom
chore/release-automation
Aug 25, 2026
Merged

chore(release): automate the ritual around goreleaser#4
dejanstrbac merged 1 commit into
mainfrom
chore/release-automation

Conversation

@dejanstrbac

Copy link
Copy Markdown
Contributor

Publishing was already automated. The ritual around it was not, and that is where both previous releases went wrong:

  • v0.1.1 published a GitHub release and its artifacts, then failed pushing the Homebrew formula (403, under-scoped token). Green-looking tag, stale formula, users silently on the old version. The formula was published by hand.
  • v0.2.0 was nearly cut from a main whose CI was red — six login tests passed on macOS and failed on the Linux runner. Nothing would have stopped the tag.

What this adds

scripts/release.shmake release VERSION=v0.2.1

Refuses before tagging: on main, clean tree, in sync with origin, tag unused, and CI green for this exact commit (matched on SHA, so a green run for an older commit cannot vouch for this one).

Verifies after publishing, because a green workflow is not proof the release is usable: assets exist, the formula reports the new version, and one really-downloaded asset's SHA-256 matches the formula. A checksum mismatch otherwise surfaces only in somebody's brew install.

release.yml — verifies the tap token can write before anything publishes. goreleaser pushes the formula last, so a bad token gives the worst outcome available. Two seconds of curl instead.

tap-token-canary.yml — weekly check. Fine-grained PATs expire after 30 days by default and nothing observes it until a release tries to publish. Now the next expiry is a failed cron on a Monday.

One detail worth knowing

homebrew-tap is public, so a token with no grant still receives 200 and the full repository JSON — only the permissions block is missing. Both checks therefore assert .permissions.push == true rather than trusting a status code. (And public visibility never removed the need for a token: reads are anonymous, writes are not.)

Verification

Refusal paths exercised directly: bad version format, wrong branch, dirty tree. The CI-green jq query was validated both ways — completed success for origin/main, missing - for an unknown SHA. All three workflow files parse. No release behaviour changes on the happy path.

🤖 Generated with Claude Code

Publishing was already automated; the ritual around it was not, and that is
where both previous releases went wrong. v0.1.1 published a GitHub release
and then failed to push the Homebrew formula, leaving a green-looking tag
and users on the old version — the formula had to be published by hand.
v0.2.0 was nearly cut from a main whose CI was red.

Three additions, each aimed at one of those:

- scripts/release.sh (make release VERSION=vX.Y.Z) refuses before it tags
  (on main, clean, in sync, tag unused, CI green FOR THIS COMMIT — matched
  on SHA so a green run for an older commit cannot vouch for this one),
  then watches the workflow and verifies what a user actually gets: release
  assets exist, the formula reports the new version, and one real asset's
  SHA-256 matches the formula. A green workflow is not proof of any of that.

- release.yml verifies the tap token can write BEFORE anything publishes.
  goreleaser pushes the formula last, so a bad token yields the worst
  available outcome: artifacts out, formula stale, run red. Two seconds of
  curl turns that into a failure that publishes nothing.

- tap-token-canary.yml checks the token weekly. Fine-grained PATs expire
  after 30 days by default and nothing observes it until a release tries to
  publish; this makes the next expiry a failed cron instead.

The tap is a public repo, so a token with no grant still gets 200 and the
full repo JSON back — only `permissions` is missing. Both checks therefore
assert `.permissions.push == true` rather than trusting a status code.

docs/RELEASING.md records the procedure and why each check exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dejanstrbac
dejanstrbac merged commit efda9d4 into main Aug 25, 2026
1 check passed
@dejanstrbac
dejanstrbac deleted the chore/release-automation branch August 25, 2026 07:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant