Round-1 blocker (claude + grok): a pull_request run from a public fork
gets a read-only GITHUB_TOKEN — permissions: cannot raise it — and every
ceremony PR this org merges is cross-repo from the bot fork, so the tag
create would 403 after green asserts, red on main per release. The door
now triggers on push to main (in-repo event, full token); the decide step
keeps the first-parent version interlock, and the release label — still
the operator's declared intent — is read via the API off the merge
commit's PR. A transition with no labeled PR behind it refuses. The doors
split on the pushed ref: tags to the tag door, main to the merge door.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The four-state table called '-dev but changed' half a ceremony and
refused — but that state IS the mandatory post-release bump PR
(bare -> X.Y.(Z+1)-dev after every release), which would have put a red
run on main once per release, forever. A tree that ends -dev is by
definition not a release: every such merge is work, and no-ops green
with a NOTICE. The red verdicts now guard only bare endstates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LABELS.md gives 'release' to release-flow WORK as well as to the ceremony
PR — the PR that added the merge door included. The old assert pair turned
every such merge into a red run on main. The fused decide step reads
VERSION against the merge commit's first parent and answers all states:
-dev unchanged = work, green NOTICE no-op; bare unchanged but already
released = work in the post-release window, same no-op; -dev-but-changed
and bare-unchanged-never-released = half-ceremonies, refused loudly;
bare-and-changed = the ceremony. Later steps gate on its output. Five new
pins in test/release.sh cover each verdict and the gating.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 0.7.0 ceremony exposed the gap: the release PR merged with four
approvals and nothing happened, correctly, because publishing hung off a
separate, manual, silent-when-forgotten tag push — the worst failure
shape, no error and no red X. The ship decision already lives in the
release PR, so the merge now IS the release.
release.yml grows a second door: pull_request closed on main, gated on
merged == true AND the hand-set release label (read from the event
payload — no extra permission). Four asserts, in order, each fail-loud
and creating nothing: VERSION at the merge commit is non--dev; VERSION
changed in this PR (merge vs first parent — the -dev interlock that
kills a mislabeled ordinary PR); the version's CHANGELOG.md section
extracts non-empty via the existing release-notes.sh; and no tag or
release exists yet. Then, in the same job, it creates the tag ref at
the merge commit via the API and publishes with gh release create
--verify-tag. Same-job on purpose: a GITHUB_TOKEN-created tag triggers
no workflows (GitHub's anti-recursion), so the tag door can never fire
off it and double-publish, and the no-existing assert covers a manual
tag racing the merge. The tag-push path stays step-for-step identical
as the documented manual fallback and backfill, gated to the push event
so a closed PR never runs it against a branch ref.
CONTRIBUTING.md's Releases section now reads "the maintainer's merge IS
the release", with the manual tag ritual kept as the fallback.
test/release.sh grep-pins the merged+labeled gate, all four asserts,
the same-job tag+publish, and that the tag-push trigger survives — in
the same daemon-free, fail-closed style.
Fixes#96
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A shape-filtered trigger silently skips the tags it doesn't match — a
habitual v0.7.0 would mint nothing and say nothing. rig's release.yml
(heavy-duty/rig#40) already triggers on '**' and lets the tag==VERSION
assert be the loud gate; align box to that.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On a bare X.Y.Z tag push (the 0.6.0 tag set the no-'v' precedent),
release.yml asserts the tag names the tree's own VERSION — a mismatch
fails loudly and creates NOTHING — then publishes the GitHub release
with that version's CHANGELOG.md section as the body: the curated
prose, not the generated PR list. No assets, because for a pure-bash
tree GitHub's source tarball for the tag IS the package.
The extraction lives in .github/scripts/release-notes.sh, a file of
its own so test/release.sh drives the same code against fixtures and
the real changelog — it refuses a missing or empty section, so a tag
whose release ritual was skipped fails before anything is created.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>