Both reviewers caught it: #41 merged after the changelog convention
landed (#40) and skipped its entry; the release PR is the last gate
before the section becomes the permanent release body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rig's first release: VERSION leaves 0.1.0-dev and the Unreleased section
is stamped with today's date. The surface was drilled twice on 2026-07-19
(offline suites, tenant guests on real incus via box, db round-trip, a
real GitHub runner lifecycle, coolify install) and the ceremony itself
rehearsed end to end on a scratch fork — including the guard that made
this very PR possible (#44).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cast-twin blocker (claude-bot): a declared permissions: block zeroes
unspecified scopes, so the label read and the bump fallback's gh pr
create could only 403 — every genuine ceremony would end red at the
label check. pull-requests: write added, consumers named. CONTRIBUTING
step 3 and the changelog entry now tell the shipped story: push-to-main
door, event.before interlock, self-re-arm, manual-path bump stays the
operator's.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rig twin of heavy-duty/box#96, from the release-ceremony retro: the
tag was a separate, manual, silent-when-forgotten step, and a forgotten
tag produces no red X — the worst failure shape. The ship decision
already lives in the release PR; merging it is "ship". After that,
tagging is transcription, and transcription belongs to machines.
release.yml now also fires on pull_request closed into main, gated on
merged AND the `release` label. The job asserts in order, each fail-loud
and creating nothing: VERSION at the merge commit is non--dev; VERSION
changed in THIS PR (base vs merge — the interlock that fails a
mislabeled ordinary PR); the changelog section for that version extracts
non-empty via the existing changelog_section from release-lib.sh; and no
tag or release exists yet. Then, in the same job, it API-creates the tag
at the merge commit and publishes the release with the extracted notes.
Same-job is load-bearing: a GITHUB_TOKEN-created tag does not fire the
tag-push trigger, so the publish must live next to the tag and the
fallback job cannot double-publish; the nothing-exists assert covers a
manual race. The tag-push path survives verbatim as the documented
manual fallback and backfill, and CONTRIBUTING's Releasing section now
reads merge-is-ship with the manual tag as fallback.
test/release.sh pins the merge path in the house grep-pin style: the
merged+labeled gate, the four asserts, the same-job tag+publish (awk
from release-on-merge: to EOF), the asserts-precede-the-tag ordering,
and the surviving tag-push trigger.
Fixes#47
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test/release.sh demanded a literal '## Unreleased' heading extracting
non-empty with '#32' in it — all false by construction on the very tree
the release PR produces, so the first real 'release: 0.1.0' PR turned CI
red and the ceremony blocked itself. Both fork rehearsals missed it: they
tag a branch, which runs release.yml and never ci.yml. The guard now
asserts its actual purpose — the TOP section, whatever its name, extracts
non-empty via the exact function release.yml runs — and passes on both
legitimate tree states (verified on main's shape and on a stamped copy).
Fixes#44
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A bare 'read -rsp' with no tty exits non-zero and set -e ends the script
with no output at all: the release drill watched 'rig runner remove' exit 1
in complete silence, and a guest bootstrap stop mid-log the same way. Every
prompt now checks for a tty first and dies naming the variable that
unblocks an unattended run; every read is || die-guarded so EOF at a real
prompt also gets a last word. The no-bare-read test swept up runner
repoint's two prompts, which the issue had not counted.
Fixes#42
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PR-then-tag half of box#83's flow, rig's side. CHANGELOG.md starts
with an Unreleased section (feature PRs land their entry as part of the
PR — box's convention, now written into CONTRIBUTING alongside the
release ritual). On a tag push, release.yml asserts the bare tag equals
the tree's own VERSION — a mismatch fails loudly and creates nothing —
then creates the GitHub release with that version's changelog section as
the body, extracted by changelog_section in .github/scripts/release-lib.sh:
one function, sourced by the workflow and driven by test/release.sh
against fixtures and the shipped CHANGELOG.md itself. No assets — for a
pure-bash tree, the tag's source tarball IS the package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>