Commit graph

7 commits

Author SHA1 Message Date
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
dan-claude-bot
d7d715ac55 chore: narrow the top-heading match with a guard, not a non-null assertion
biome (error-on-warnings in CI) rejects the ! assertion; an explicit
throw narrows properly and says what broke if the file ever has no
heading at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:52:10 +00:00
dan-claude-bot
f9187272ec fix: the release suite accepts the ceremony's own tree (#108)
release.test.ts demanded the real changelog's literal Unreleased section
extract non-empty containing '#96' — 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. Fork rehearsals missed it:
a tag push runs release.yml, never ci.yml. The guard now asserts the TOP
section, whatever its name, extracts non-empty via the exact tool
release.yml runs — verified on both legitimate tree states.

Fixes #108

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:50:03 +00:00
dan-claude-bot
ced5c497c0 feat: release flow — tagged releases with a prebuilt dist asset (#96)
The cast half of the flow designed in heavy-duty/box#83, aligned with
box#90 and rig#40, plus the piece unique to cast: a prebuilt release
asset, because cast is the one repo where the source tarball is not the
package.

- CHANGELOG.md (box's format) with this PR's entry under Unreleased;
  feature PRs land their entry as part of the PR.
- `cast --version` / `-V` answers with package.json's version, read
  relative to the compiled module so a source checkout and an installed
  prebuilt tree agree.
- release.yml, on EVERY tag push (no shape filter — a mismatched tag
  must fail the assert loudly, not be pattern-skipped): asserts tag ==
  package.json version FIRST, extracts that version's changelog section
  (.github/scripts/release-notes.sh, shared with the tests; missing or
  empty refuses), builds once (npm ci && npm run build && npm prune
  --omit=dev), stages bin/ dist/ node_modules/ package.json as
  cast-X.Y.Z/ and attaches cast-X.Y.Z.tgz to `gh release create
  --verify-tag`. No tests here — ci.yml gated the merge commit, and the
  suite needs age.
- install.sh grows the three channels: default = the latest release's
  asset (tag resolved off the releases/latest redirect Location — no
  API, no token; failure dies loudly naming CAST_REF=main, never a
  silent fallback), CAST_REF=<tag> = pinned (asset first, source
  fallback), CAST_REF=main = dev build-from-source. npm is required
  only on the source path, and a prebuilt tree is sanity-checked
  (dist/, node_modules/) before $DEST is replaced.
- test/release.test.ts drives it all offline: --version, the extraction
  against fixtures (0.7.0 never matches 0.7.0-rc1) and the real
  changelog, and REAL install.sh runs through all three channels with a
  stub curl and a poisoned npm — including the loud no-releases refusal
  with no $DEST side effects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:27:55 +00:00