fix: pull-requests scope for the door's two PR-API calls; docs catch up

The permission-starvation blocker found on the cast twin (claude-bot,
cast#112 round 4) is identical here: a declared permissions: block zeroes
every unspecified scope, so the decide step's 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 with the consumers
named. CONTRIBUTING step 3 and the changelog entry now tell the shipped
story: push-to-main door, event.before interlock, self-re-arm with the
manual path's bump staying the operator's. Re-runs wording nit taken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dan-claude-bot 2026-07-19 16:33:41 +00:00
parent 03e1a8c5b7
commit b89ed144fe
3 changed files with 38 additions and 23 deletions

View file

@ -40,7 +40,11 @@ on:
tags: ["**"] tags: ["**"]
permissions: permissions:
contents: write # create the tag ref + gh release create contents: write # create the tag ref + gh release create + the bump push
# Two consumers (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:
# The merge door (#96), riding pushes to main (see the trigger comment: # The merge door (#96), riding pushes to main (see the trigger comment:
@ -120,7 +124,7 @@ jobs:
run: | run: |
bash .github/scripts/release-notes.sh "$(cat VERSION)" > "$RUNNER_TEMP/notes.md" bash .github/scripts/release-notes.sh "$(cat VERSION)" > "$RUNNER_TEMP/notes.md"
cat "$RUNNER_TEMP/notes.md" cat "$RUNNER_TEMP/notes.md"
- name: nothing may exist yet — no tag, no release (idempotency) - name: nothing may exist yet — no tag, no release (re-runs refuse loudly)
if: steps.decide.outputs.ceremony == 'yes' if: steps.decide.outputs.ceremony == 'yes'
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}

View file

@ -7,23 +7,30 @@ which records not just what changed but what each drill run proved.
### Added ### Added
- **Merging the release PR IS the release** (#96) — the 0.7.0 ceremony ended - **Merging the release PR IS the release — and the release re-arms main
in an absence: the release PR merged with four approvals and nothing itself** (#96) — the 0.7.0 ceremony ended in an absence: the release PR
happened, correctly, because publishing hung off a separate, manual, merged with four approvals and nothing happened, correctly, because
silent-when-forgotten tag push — a failure shape with no error and no red publishing hung off a separate, manual, silent-when-forgotten tag push —
X. The ship decision already lives in the release PR (the one PR whose a failure shape with no error and no red X. The ship decision already
whole diff is "the version leaves `-dev`"), so `release.yml` now fires on lives in the release PR (the one PR whose whole diff is "the version
a merged, `release`-labeled PR into main: the label is the intent, the leaves `-dev`"), so `release.yml` now fires on pushes to main
version transition is the interlock — `VERSION` at the merge commit must (fork-sourced ceremony PRs get a read-only token on `pull_request`
be non-`-dev` AND must have changed in this PR, so a mislabeled ordinary events), reading the transition from the push itself: `event.before` to
PR fails loudly and creates nothing — the notes must extract from the the pushed head. A decide step answers four states — release-flow *work*
changelog, and no tag or release may exist yet. Then, in the same job, it merged under the `release` label (`-dev` endstates, the post-release
tags the merge commit via the API and publishes; same-job on purpose, window) no-ops green with a NOTICE; the two genuinely ambiguous bare
because a `GITHUB_TOKEN`-created tag triggers no workflows, which is also states refuse loudly; a true transition then requires a merged,
what makes double-publish impossible. The tag-push path stays unchanged as `release`-labeled PR behind the commit (read via the API — the label is
the documented manual fallback and backfill (it shipped 0.7.0 itself). the operator's declared intent) before anything is created. Then, in the
`test/release.sh` grep-pins the merged+labeled gate, all four asserts, and same job, it tags the merge commit via the API, publishes — and bumps
the same-job tag+publish in the same daemon-free, fail-closed style. 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. Same-job on
purpose: a `GITHUB_TOKEN`-created tag triggers no workflows, which is
also what makes double-publish impossible. The tag-push path stays
unchanged as the documented manual fallback and backfill (it shipped
0.7.0 itself). `test/release.sh` grep-pins the gate, every decide
verdict, the single `on.push` key, and the same-job tag+publish+re-arm
in the same daemon-free, fail-closed style.
## 0.7.0 — 2026-07-19 ## 0.7.0 — 2026-07-19

View file

@ -70,10 +70,14 @@ A release is a PR, and merging it ships it
commit bare `X.Y.Z` by hand and pushing the tag still publishes the same commit bare `X.Y.Z` by hand and pushing the tag still publishes the same
way (release.yml asserts the tag names the tree's own `VERSION`) — for way (release.yml asserts the tag names the tree's own `VERSION`) — for
backfills, or the day the merge path is red. backfills, or the day the merge path is red.
3. **Immediately after: bump `main`'s `VERSION` to `X.Y.(Z+1)-dev`.** Not 3. **The release re-arms main itself**: the same workflow run bumps
cosmetic — the versioned layout names install trees after `VERSION`, so a `VERSION` to `X.Y.(Z+1)-dev` and pushes the commit straight to main —
`main` install without the bump would land in `versions/X.Y.Z` and no follow-up PR (it opens one only if branch protection refuses the
impersonate the release you just cut. direct push, and says so loudly). Not cosmetic — the versioned layout
names install trees after `VERSION`, so a `main` install without the
bump would land in `versions/X.Y.Z` and impersonate the release just
cut. On the *manual* tag path the bump stays yours: open the one-line
PR after publishing.
## Labels — who sets what ## Labels — who sets what