feat(ci): release on a VERSION change instead of on a tag push - #41
Conversation
Tagging was the human gate when the tag triggered the deploy. That stopped
being true once Workers Builds began deploying worker/ from main: the act that
decides what users receive is merging a reviewed VERSION and PINS change, both
CODEOWNERS-gated. Tagging afterwards added no judgement, only a step that could
be forgotten — leaving VERSION bumped, installers stamped, and no release.
Automating the tag push does not work, and the reasons are recorded in the
workflow so nobody rediscovers them the hard way:
1. the release-tags ruleset rejects tag creation by anyone outside
installer-maintainers, bots included;
2. a tag pushed with GITHUB_TOKEN starts no workflow run — the same
suppression that would have left every pin-bump PR unmergeable.
So the trigger is inverted rather than the push automated. The workflow runs
because VERSION changed and creates the tag as an output of its own work, so
nothing waits on a tag and (2) cannot apply. Only (1) remains, as a one-time
ruleset change.
Tag creation is idempotent: an existing tag at the same commit continues, and
one pointing elsewhere fails loudly rather than republishing different bytes
under a version people already pinned. Release creation and the bump PR are
idempotent too, so re-running a partly-failed release is safe.
Ordering: stamp --check runs before the tag is created, so an unstamped commit
never gets one. Pins are computed after, since gen-pins derives them from tags.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
get-resq-software | 275ed42 | Aug 11 2026, 04:28 AM |
|
Warning Review limit reached
Next review available in: 51 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. 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: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe release process now starts from merged ChangesAutomated release flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant VERSION change
participant verify job
participant Git tag
participant release job
participant pin-bump pull request
participant Workers Builds
VERSION change->>verify job: trigger workflow
verify job->>Git tag: create or validate tag
verify job->>release job: pass tag, version, and pins
release job->>pin-bump pull request: create or update pin changes
pin-bump pull request->>Workers Builds: trigger deployment verification after merge
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
SC2016: a literal $REL inside single quotes, which actionlint fails the build on. Same class as the repo-drift sed patterns earlier. Escaping it would read worse than expanding $VERSION directly, and full URLs are better release notes anyway — the reader can paste them without first setting a variable. The "verify against one immutable release" property is unchanged: both URLs carry the same version. The jq programs elsewhere in this file also contain $ in single quotes and are not flagged; shellcheck knows jq takes a literal script. Those lines passed actionlint on the previous four PRs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Line 97: Redirect all diagnostic echo messages in
.github/workflows/release.yml at lines 97-97, 122-124, 201-203, 290-294, and
323-327 to stderr, covering release, tag, existing-release, branch/PR, and
dispatch status messages. Leave any $GITHUB_OUTPUT writes unchanged.
- Around line 42-47: Restrict the manual-release path in the workflow before tag
creation: permit new releases only when github.ref is refs/heads/main, while
allowing recovery only from an existing matching release tag or an immutable
commit SHA. Dereference annotated tags and compare the resolved target commit
with GITHUB_SHA, and make verify/tag creation fail for any other ref.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9a4ffb85-c2d2-4ef5-b161-48f5701b817b
📒 Files selected for processing (2)
.github/workflows/release.ymlAGENTS.md
…into feat/release-on-version-change
PR #40 silently reverted bin/gen-pins.sh. The `|| true` on the grep -c count was present in 3cf1268, 017ec1b and cc65869, and absent from bb05ab1. My fault: I used `git add -A` when creating that branch, which staged a stale copy of a file the PR had no business touching. Without it, grep -c exits 1 on zero matches, the command substitution inherits that status, and set -e kills the script one line before the die that explains the problem — so the guard is unreachable in exactly the case it exists for, and a missing PINS block becomes a silent nonzero exit. Restoring it is the small half. The real problem is that a fix with no test disappeared and nothing noticed for three merges, so required.yml now asserts the guard is reachable. Verified both directions: with the fix the check reports "found 0"; with the revert applied it produces no output at all and the check fails. The test asserts on the message rather than on a source line, so a future rewrite still has to preserve the behaviour rather than just the spelling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed two things beyond the actionlint fix.
A regression I caused in #40. Merging Without it the guard is unreachable in exactly the case it exists for — Restoring it is the small half. The real problem is that a fix with no test vanished and nothing noticed across three merges. |
workflow_dispatch lets a caller pick any ref, and this workflow did not check github.ref before creating a tag with contents: write. Anyone with write access could have pushed a branch carrying a bumped VERSION and an altered install.sh, dispatched a release, and obtained a tag plus a published Release with SHA256SUMS — from code that was never reviewed. That is precisely the gate the trigger inversion was meant to enforce, so the hole was in the same change that argued for it. Now only refs/heads/main, or refs/tags/v$VERSION for recovery. The tag path cannot publish anything new: the tag step finds it already pointing at that commit and continues, and tag creation is itself restricted by the release-tags ruleset, so the ref is already privileged. Exercised: main and refs/tags/v0.4.0 allow; refs/heads/attacker, refs/tags/v9.9.9 and refs/pull/99/merge block. The guard runs before tag creation, not after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/required.yml:
- Around line 58-81: Update the “gen-pins missing-PINS guard is reachable”
workflow step to capture and validate gen-pins.sh’s exit status using an if
condition, failing the test if the command succeeds. When it fails, assert the
complete diagnostic emitted at bin/gen-pins.sh line 122 rather than only
matching “found 0”; preserve the existing error reporting for unexpected output.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e0996fac-8fd5-49e5-b4c3-120d7c7324dd
📒 Files selected for processing (3)
.github/workflows/release.yml.github/workflows/required.ymlbin/gen-pins.sh
🚧 Files skipped from review as they are similar to previous changes (1)
- .github/workflows/release.yml
The guard test checked only the message and swallowed the status with `|| true`. A script that printed the diagnostic and exited 0 would have passed it — while being broken in exactly the way that matters, since the guard exists to stop the run rather than to narrate. Confirmed the gap is real: given a stub that prints the message and exits 0, the old check passes and the new one fails the build. Now asserts the status first, then matches the complete diagnostic rather than the "found 0" fragment, so a message drifting into a different meaning cannot keep the check green. Verified: correct code passes; the revert (no `|| true`) fails with no output; print-then-exit-0 fails on the status. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ag (#42) The first real release failed: v0.4.0 already exists at {"message":"Not Found",...,"status":"404"} but this commit is e837c3c `gh api` writes its error body to stdout on a 404, and only stderr was redirected. So `existing` was set to a JSON error object rather than the empty string, the "already exists" branch fired, and the run reported a collision with a tag that does not exist. Every first release of any version would have hit this. Reproduced against the live API before and after: old logic yields the 404 body, new logic yields empty and proceeds to create. Also dereference annotated tags, which CodeRabbit raised on #41 and I skipped on the reasoning that it only affected the recovery path. It affects this check too: an annotated tag's ref points at a tag object, so comparing that id with GITHUB_SHA always differs and reads as a collision. Not hypothetical — resq-software/crates carries annotated tags, and a maintainer tagging by hand usually produces one. The failure was safe: no tag created, nothing published, both later jobs skipped. Whether github-actions[bot] can bypass the release-tags ruleset is still unknown, because the run never reached the create call. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Answers "shouldn't the tag push be automatic?" — yes, and my original reasoning had expired.
Why the old design was wrong
I made tagging the human gate when the tag triggered the deploy. Once Workers Builds started deploying
worker/frommain, the real gate became merging a reviewedVERSION/PINSchange — already CODEOWNERS-gated. Tagging afterwards added no judgement, only a step that could be forgotten, leavingVERSIONbumped, installers stamped, and no release to match.Why I did not simply automate the tag push
Two blockers, both recorded in the workflow so nobody rediscovers them painfully:
release-tagsruleset rejectsrefs/tags/v*creation by anyone outside@resq-software/installer-maintainers— bots included.GITHUB_TOKENstarts no workflow run. Same suppression that would have left every pin-bump PR without its required check.A workflow that pushed a tag would hit (1); if it got past that, it would create a tag that triggered nothing.
What this does instead
Inverts the trigger. The workflow runs because
VERSIONchanged and creates the tag as an output of its own work:Nothing waits on a tag, so (2) cannot apply.
Idempotent throughout, because a partly-failed release has to be re-runnable: an existing tag at the same commit continues; one pointing elsewhere fails loudly rather than republishing different bytes under a version people already pinned; the release refreshes instead of erroring; the bump PR updates in place.
Needs from you, once
That reads like a loosening but is stricter in practice: it moves the privileged act from "create a tag" — which today any maintainer can do on any commit with no review — to "merge a reviewed
VERSIONchange".I have not run it. Org rules are yours to change.
Verified
YAML valid, 3 jobs, 0
secrets.*references. The VERSION shape guard accepts0.4.0and rejects1..2,1.2,1.2.3.4,.,abc— matchingbin/stamp.sh.AGENTS.mdupdated; no manual tag instructions remain anywhere in the docs.Stacked note: independent of #40 (worker-live window), which touches only
worker-live.yml.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Process
Bug Fixes
Documentation