From 66b136efc039380d8895948f187ebeaba9937f35 Mon Sep 17 00:00:00 2001 From: Andriujose <43181885+andriujoseba@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:31:34 +0000 Subject: [PATCH] docs: wire refs guard into ceremony flow --- .github/labeler.yml | 3 +++ BUILDER.md | 7 ++++++ REVIEWER.md | 7 +++++- actions/refs-not-closing/action.yml | 6 ++++-- changelog.d/218.md | 4 ++++ docs/CONSUMERS.md | 33 +++++++++++++++++++++++++++-- test/refs-not-closing.test.sh | 4 ++++ 7 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 changelog.d/218.md diff --git a/.github/labeler.yml b/.github/labeler.yml index 1859654..aa1a7aa 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -32,9 +32,12 @@ scope:guards: - actions/changelog-armed/** - actions/changelog-monotonic/** - actions/drill-recorded/** + - actions/refs-not-closing/** + - .github/workflows/refs-guard.yml - test/changelog-armed.test.sh - test/changelog-monotonic.test.sh - test/drill-recorded.test.sh + - test/refs-not-closing.test.sh scope:labels: - changed-files: - any-glob-to-any-file: diff --git a/BUILDER.md b/BUILDER.md index 9066716..d8376ea 100644 --- a/BUILDER.md +++ b/BUILDER.md @@ -182,6 +182,13 @@ triage bug, and the move is to say so on the issue, not to guess. absent that instruction `Closes #N` remains the default. The exception was bought the hard way: #143 carried `Closes #137` as doctrine then required, and the merge closed #137 with its post-merge criterion unmet (#151). + On a `Refs #N` PR, never put a closing keyword (`close`, `closes`, + `closed`, `fix`, `fixes`, `fixed`, `resolve`, `resolves`, `resolved`) + immediately before `#N` anywhere in the body — including the sentence + explaining why the PR does not close it. GitHub reads the whole body by + adjacency, not intent. Put the number first (`#N is closed by hand`) or + omit it (`triage closes the issue by hand`). A code span does not protect + the phrase: a backticked `Closes #199` still closed #199 (#200, #218). Drafts are invisible to the reviewer panel on purpose — the draft phase is yours. - **The issue's acceptance criteria are your definition of done.** Reproduce diff --git a/REVIEWER.md b/REVIEWER.md index f41565b..7d8e74e 100644 --- a/REVIEWER.md +++ b/REVIEWER.md @@ -34,7 +34,12 @@ In order of authority: not a defect: the issue directs it, triage owns that close, and a request-changes on the "missing" keyword enforces the bug the shape exists to fix — `Closes #137` closed its issue with a post-merge - criterion unmet (#151). Check every + criterion unmet (#151). For a `Refs #N` body, also verify that no closing + keyword immediately precedes `#N` anywhere in the body, even in prose + explaining the hand close or inside a code span: GitHub used those exact + shapes to close #209, #212 and #199 (#200, #218). The safe forms put the + number first (`#N is closed by hand`) or omit it (`triage closes the issue + by hand`). Check every criterion; a PR that ships less than the issue says is a request-changes even if the code is beautiful. 2. **The repo's load-bearing constraints** — the rules bought with diff --git a/actions/refs-not-closing/action.yml b/actions/refs-not-closing/action.yml index 22c1f3f..7dc795a 100644 --- a/actions/refs-not-closing/action.yml +++ b/actions/refs-not-closing/action.yml @@ -44,9 +44,11 @@ runs: .data.repository.pullRequest | if . == null then error("pull request was not returned") else .body // "" end ' <<<"$facts" >"$body_file" - jq -er ' + jq -r ' .data.repository.pullRequest.closingIssuesReferences - | if .pageInfo.hasNextPage then + | if . == null then + error("closing issue references were not returned") + elif .pageInfo.hasNextPage then error("more than 100 closing issue references; refusing a partial verdict") else .nodes[].number diff --git a/changelog.d/218.md b/changelog.d/218.md new file mode 100644 index 0000000..cdbdc46 --- /dev/null +++ b/changelog.d/218.md @@ -0,0 +1,4 @@ +### Added + +- Pull requests that promise `Refs #N` now fail a read-only, body-edit-aware + guard if GitHub would close N through a keyword or sidebar link (#218). diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 437d64b..fc12b4c 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -124,14 +124,41 @@ the machinery at all: consumer. In particular, `0.1.0` carries `changelog-armed`, `changelog-monotonic` and `drill-recorded` plus `docs-sync`, but not `changelog-assembled` or `runner-isolated`. -6. **Labels automation** (optional but recommended): the two callers from +6. **`.github/workflows/refs-guard.yml`** — the body-aware guard is its own + caller because `edited` is load-bearing: #200 gained its accidental + closing keyword after the PR opened, with no push to wake ordinary CI. + It costs the consumer one read-only workflow file and no other machinery: + + ```yaml + name: Refs guard + + on: + pull_request: + types: [opened, edited, reopened, synchronize] + + permissions: + contents: read + pull-requests: read + + jobs: + refs-not-closing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: heavy-duty/ceremony/actions/refs-not-closing@ + ``` + + `refs-not-closing` is **unreleased** until the first tag carrying #218. + Adopt this caller with that ordinary pin bump; never point only this file + at a moving or newer ref. +7. **Labels automation** (optional but recommended): the two callers from [Labels automation](#labels-automation) — the event-facing labels caller and the sweep caller (#209) — plus `.github/labels.conf` (panel + the repo's `scope:*` rows) and `.github/labeler.yml` (the path→scope globs). Run the sweep caller's `workflow_dispatch` once — **this bootstraps the taxonomy, `release` label included** — and use it again whenever an operator needs a full-board sweep immediately. -7. **The artifact hook** (optional): `.github/actions/release-artifact/` +8. **The artifact hook** (optional): `.github/actions/release-artifact/` per [The artifact hook](#the-artifact-hook). No hook → the source tarball is the package. @@ -156,6 +183,8 @@ precisely so the machinery is safe to work on sibling `push:` silently kills a door (rig's review catch). - [ ] Swap the guard *script* steps in `ci.yml` for the `uses:` steps in the bootstrap list above (with `fetch-depth: 0` on the checkout). +- [ ] Add `refs-guard.yml` from the bootstrap list with the same ceremony + pin as the release caller and CI guard steps. - [ ] Replace `labels.yml` with the caller from [Labels automation](#labels-automation) and add the sweep caller `labels-sweep.yml` beside it (#209); extract diff --git a/test/refs-not-closing.test.sh b/test/refs-not-closing.test.sh index 71696d7..04a12c6 100755 --- a/test/refs-not-closing.test.sh +++ b/test/refs-not-closing.test.sh @@ -64,6 +64,10 @@ check "#214 incident replays red" 1 "#212" guard incidents-214 212 body incidents-200 'Refs #199' 'A later edit added `Closes #199`.' check "#200 incident replays red" 1 "#199" guard incidents-200 199 +body multiple 'Refs #5 and Refs #7.' 'Triage closes #5 and fixes #7 by hand.' +check "failure names every intersecting issue" 1 \ + "scheduled to close: #5 #7" guard multiple 5 7 + for number in 207 191 190 176 165 164; do body "incident-$number" "Refs #$number" check "#$number incident replays green" 0 "no Refs target" \