feat: merging a release-labeled PR is the release #112

Merged
dan-claude-bot merged 9 commits from feat/release-on-merge into main 2026-07-19 17:12:19 +00:00

9 commits

Author SHA1 Message Date
dan-claude-bot
c55e6bfc7c fix: issues: write — the fallback PR's --label rides the issues API (labels.yml precedent)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:35:37 +00:00
dan-claude-bot
b92919d4d0 fix: pull-requests scope for the door's two PR-API calls; docs catch up to the shipped shape
claude-bot's round-4 blockers: (1) a declared permissions: block zeroes
every unspecified scope, so the decide step's label read
(commits/<sha>/pulls) and the bump fallback's gh pr create could only
403 — every genuine ceremony would end red at the label check, the exact
failure shape this feature exists to kill, one layer down. labels.yml
already carries the precedent; pull-requests: write added with the two
consumers named. (2) CONTRIBUTING still prescribed the follow-up bump PR
the workflow now performs itself, and the changelog entry described the
old PR-base interlock — both now tell the shipped story (event.before
interlock, self-re-arm, manual-path bump stays the operator's). Nits
taken: fetch-depth 2 for the all-zeros fallback's first parent, re-runs
refuse-loudly wording, the bump-window arithmetic comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:31:40 +00:00
dan-claude-bot
a5d108cbf2 test: quantifier form in the on.push pin — biome rejects literal double spaces
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:18:51 +00:00
dan-claude-bot
d4409c3f42 fix: one on.push key — YAML last-key-wins had dropped the tag door; format the pins
grok's round-2 catches: (1) two sibling push: maps under on: leave only
the second alive — the tag-push fallback stopped triggering entirely;
both filters now live under one push key with the steps still split on
the pushed ref, and a pin counts exactly one on.push. (2) CI red was the
unformatted pin block — biome now clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:16:08 +00:00
dan-claude-bot
121aa6bd5b feat: the release re-arms main — the -dev bump folds into the release act
Operator decision: the post-release bump PR is ceremony debris — a
derivable one-liner (package.json + lock, via npm, never regex) with no
judgment for a review to add. After tag + build + publish, the same job
computes X.Y.(Z+1)-dev and pushes it to main directly (a GITHUB_TOKEN
push fires no workflows: no recursion, no red run); if branch protection
refuses, the step opens the bump PR itself, loudly. Merge-door only —
the manual tag fallback does not rewrite main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:04:55 +00:00
dan-claude-bot
84bd9f341f fix: the merge door rides pushes to main — fork PR tokens are read-only
Round-1 blocker (grok; claude's box twin): 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 reads the transition from event.before (first-
parent fallback for the all-zeros edge) and the release label — still
the operator's declared intent — via the API off the merge commit's PR.
A transition with no labeled PR behind it refuses. The steps split on
the pushed ref: tags to the tag path, main to the merge path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:47:34 +00:00
dan-claude-bot
fcb7ce3b7d fix: a -dev endstate is always work — the post-release bump must not run red
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), 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, green NOTICE no-op. Red now guards only bare
endstates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:42:06 +00:00
dan-claude-bot
a5910eacee fix: the release label's two meanings part ways in a decide step
LABELS.md gives 'release' to release-flow WORK as well as to the ceremony
PR — the PR that added the merge path included. The old assert pair turned
every such merge into a red run on main. The fused decide step reads the
version against the PR base and answers all states: -dev unchanged = work,
green NOTICE no-op; bare unchanged but already released = work in the
post-release window (cast's whole pre-0.1.1 era included), same no-op;
-dev-but-changed and bare-unchanged-never-released = half-ceremonies,
refused loudly; bare-and-changed = the ceremony. Shared steps gate on the
decide (tag-push path unaffected). Pins anchor on the echo strings, since
the workflow's own comment table paraphrases the states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:28:58 +00:00
dan-claude-bot
775af63063 feat: merging a release-labeled PR is the release (#111)
box#95 taught the family that a forgotten manual tag is the worst
failure shape: silent, no red X, a release that simply doesn't happen.
The ship decision already lives in the ceremony PR — the one whose whole
diff is the version leaving -dev, carrying the reviews and the
maintainer's merge — so tagging after it is transcription, and
transcription belongs to the machine (box#96's design; this is cast's
twin).

release.yml now also triggers on pull_request closed against main,
gated on merged == true AND the hand-set release label. The merge path
asserts four facts in order, each fail-loud and creating nothing: the
merged package.json version is non--dev (read via node, never regex —
the pkg_version discipline); the version CHANGED in this PR (base vs
merge — the -dev interlock, so a mislabeled ordinary PR fails loudly);
the version's changelog section extracts non-empty via the existing
release-notes.sh; and no tag or release exists yet (idempotent re-runs,
and the loud answer to a manual-tag race). Then, in the same job, it
tags the merge commit via the API and publishes. Same-job is
load-bearing: a GITHUB_TOKEN-created tag triggers no workflows, so the
tag-push path cannot fire on it and double-publish.

Both trigger paths converge on literally the same steps — each entry
step exports RELEASE_VERSION, and the notes extraction, the exact
existing asset build (npm ci, npm run build, npm prune --omit=dev,
staged as cast-X.Y.Z/), and the gh release create read only that — so
the paths cannot drift and the installer keeps finding the one asset
name it knows, cast-X.Y.Z.tgz. The tag-push path survives as the
documented manual fallback and backfill, and it matters immediately:
0.1.0 never carried -dev (cast predates the ritual), so the interlock
correctly does not fire for #110's ceremony — that one ships by manual
tag, and the automation applies from 0.1.1 on.

test/release.test.ts pins the new wiring in the house grep style,
fail-closed: the merged+labeled gate, the four asserts strictly ordered
ahead of tag/build/publish, the single job, the anti-recursion comment,
and that no per-path asset name exists. CONTRIBUTING.md's Releasing now
says it plainly: merge is the ship decision; the tag is the fallback.

Fixes #111

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:21:07 +00:00