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: ["**"]
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:
# The merge door (#96), riding pushes to main (see the trigger comment:
@ -120,7 +124,7 @@ jobs:
run: |
bash .github/scripts/release-notes.sh "$(cat VERSION)" > "$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'
env:
GH_TOKEN: ${{ github.token }}

View file

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

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
way (release.yml asserts the tag names the tree's own `VERSION`) — for
backfills, or the day the merge path is red.
3. **Immediately after: bump `main`'s `VERSION` to `X.Y.(Z+1)-dev`.** 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 you just cut.
3. **The release re-arms main itself**: the same workflow run bumps
`VERSION` to `X.Y.(Z+1)-dev` and pushes the commit straight to main —
no follow-up PR (it opens one only if branch protection refuses the
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