From b89ed144fe6f2706e4b40733925f2aa9bfad6b98 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sun, 19 Jul 2026 16:33:41 +0000 Subject: [PATCH] fix: pull-requests scope for the door's two PR-API calls; docs catch up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/release.yml | 8 +++++-- CHANGELOG.md | 41 ++++++++++++++++++++--------------- CONTRIBUTING.md | 12 ++++++---- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe2ad40..18810ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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//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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 38767f0..f238a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8eb0a9..394985f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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