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>
This commit is contained in:
dan-claude-bot 2026-07-19 16:31:40 +00:00
parent a5d108cbf2
commit b92919d4d0
3 changed files with 43 additions and 20 deletions

View file

@ -39,7 +39,11 @@ on:
branches: [main] branches: [main]
permissions: permissions:
contents: write # tag create via the API + gh release create contents: write # tag create via the API + gh release create + the bump push
# Two consumers (labels.yml precedent — a declared permissions: block
# zeroes every unspecified scope): the decide step's label read
# (commits/<sha>/pulls) and the bump fallback's `gh pr create --label`.
pull-requests: write
jobs: jobs:
release: release:
@ -58,6 +62,10 @@ jobs:
# merge commit the maintainer shipped, which the tag created # merge commit the maintainer shipped, which the tag created
# below will name). # below will name).
ref: ${{ github.sha }} ref: ${{ github.sha }}
# Depth 2: the pushed head's first parent must be resolvable for
# the decide step's all-zeros fallback (event.before on a
# branch-creation push).
fetch-depth: 2
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "22" node-version: "22"
@ -159,9 +167,10 @@ jobs:
MERGE_SHA: ${{ github.sha }} MERGE_SHA: ${{ github.sha }}
run: | run: |
# Assert 4 — no tag and no release exist for this version. Re-runs # Assert 4 — no tag and no release exist for this version. Re-runs
# stay idempotent, and a manual race (an operator who tagged by # of a completed ceremony REFUSE LOUDLY (red, creating nothing —
# hand between merge and here) fails loudly instead of # the correct direction), and a manual race (an operator who
# double-publishing. # tagged by hand between merge and here) fails the same way
# instead of double-publishing.
if git ls-remote --exit-code origin "refs/tags/$RELEASE_VERSION" > /dev/null; then if git ls-remote --exit-code origin "refs/tags/$RELEASE_VERSION" > /dev/null; then
echo "tag '$RELEASE_VERSION' already exists — creating nothing (already released, or a manual tag won the race)." >&2 echo "tag '$RELEASE_VERSION' already exists — creating nothing (already released, or a manual tag won the race)." >&2
exit 1 exit 1
@ -214,6 +223,10 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
# next is computed from the RELEASE tree (the checkout), then
# applied to whatever main is by the time of the push — if main
# moved in the window, release+1 still lands on the newer head,
# which is the intended arithmetic either way.
next="$(node -p 'const v = require("./package.json").version.split("."); v[2] = String(Number(v[2]) + 1) + "-dev"; v.join(".")')" next="$(node -p 'const v = require("./package.json").version.split("."); v[2] = String(Number(v[2]) + 1) + "-dev"; v.join(".")')"
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"

View file

@ -9,17 +9,24 @@ actually cutting it, and this file starts there.
### Added ### Added
- **Merging a release-labeled PR is the release** (#111; box#96's design) — - **Merging a release-labeled PR is the release — and the release re-arms
`release.yml` now also fires when a `release`-labeled PR merges into main itself** (#111; box#96's design) — `release.yml` now also fires on
main: it asserts fail-loud (non-`-dev` version, version changed in the pushes to main (not `pull_request` events: fork-sourced ceremony PRs get
PR — the `-dev` interlock, changelog section extracts non-empty, no a read-only token there — the round-1 catch). A decide step reads the
existing tag or release), then tags the merge commit, builds the version transition from the push (`event.before` → the pushed head) and
`cast-X.Y.Z.tgz` asset once, and publishes — the maintainer's merge is answers four states: release-flow *work* merged under the `release`
the ship decision, no silent-when-forgotten manual tag step. The label — `-dev` endstates, and the post-release window — no-ops green
tag-push path stays as the documented fallback and backfill, and both with a NOTICE; the two genuinely ambiguous bare states refuse loudly;
paths run the same steps so they cannot drift. First-release edge: a true transition then requires a merged, `release`-labeled PR behind
0.1.0 never carried `-dev`, so its ceremony (#110) ships by manual tag; the commit (read via the API — the label is the operator's declared
the automation applies from 0.1.1 on. intent) before the door opens. It then tags the merge commit, builds
the `cast-X.Y.Z.tgz` asset once, publishes — and bumps main to
`X.Y.(Z+1)-dev` itself, direct push with a loud open-a-PR fallback, so
no follow-up bump PR exists on the paved road. The tag-push path stays
as the documented fallback and backfill, and both paths run the same
steps so they cannot drift. First-release edge: 0.1.0 never carried
`-dev`, so its ceremony (#110) ships by manual tag; the automation
applies from 0.1.1 on.
### Fixed ### Fixed

View file

@ -69,12 +69,15 @@ on box#83's shape):
*Manual fallback and backfill:* push a bare `X.Y.Z` tag on the merge *Manual fallback and backfill:* push a bare `X.Y.Z` tag on the merge
commit yourself — the same workflow runs the same asserts, build, and commit yourself — the same workflow runs the same asserts, build, and
publish from the tag. publish from the tag.
3. **Right after the release, a follow-up PR bumps `package.json` to 3. **The release re-arms main itself**: the same workflow run bumps
`X.Y.(Z+1)-dev`** (and `package-lock.json` with it) — box#90's step of `package.json` (and `package-lock.json`) to `X.Y.(Z+1)-dev` and pushes
the family ritual. Installs are versioned by the tree's `package.json` the commit straight to main — no follow-up PR (it opens one only if
version, so a `CAST_REF=main` install between releases must land as branch protection refuses the direct push, and says so loudly).
Installs are versioned by the tree's `package.json` version, so a
`CAST_REF=main` install between releases must land as
`versions/X.Y.(Z+1)-dev`, never as `versions/X.Y.Z` — main's tree must `versions/X.Y.(Z+1)-dev`, never as `versions/X.Y.Z` — main's tree must
not impersonate the release it merely descends from. not impersonate the release it merely descends from. On the *manual*
tag path the bump stays yours: open the one-line PR after publishing.
## Labels — who sets what ## Labels — who sets what