Skip to content

Commit 0c5c3f0

Browse files
committed
fix(pm): the archiver's workflow exercises itself at PR time, writing nothing
`check:pm-dispatch-gates` pins that no discovered gate family is reachable only through a schedule — every patrol in this tree carries a `pull_request` paths trigger, and a workflow whose script CI never runs at PR time reddens that gate by existing. The first draft of `board-snapshot.yml` omitted the trigger to keep PR-time cost at zero and was refused by exactly that pin, which named the remedy in its own failure text. So a pull request now runs the snapshot on a real runner — transport, flags, page walk and rendering, proven where they will actually run — into the runner's temp dir, `--dry-run`, capped at five numbers and twenty-five requests, with the archive checkout and the commit both skipped. A pull request writes to neither the board nor the archive, and the two spellings differ in where they write and in nothing else, so what a PR exercises is the path a schedule takes. Two readings from the local drill, folded back in: the PENDING count-check line no longer prints an arithmetic the run never read (a stopped run skips the board's own count, and `expected 0` read as a real zero), and `--restore` no longer re-execs itself through the proxy — it makes no request at all, so routing its transport spawned a child to prove a route that mode never uses. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude <noreply@anthropic.com>
1 parent a6bd2f4 commit 0c5c3f0

2 files changed

Lines changed: 52 additions & 11 deletions

File tree

‎.github/workflows/board-snapshot.yml‎

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,28 @@ name: Board Snapshot
4343
# cursor and exits non-zero with the reset time. A loop against a spent budget
4444
# starves every other automated caller in the repository for the rest of the hour.
4545
#
46-
# ## No `pull_request` trigger, deliberately
46+
# ## The `pull_request` trigger, and what it is allowed to do
4747
#
48-
# The sibling patrol proves itself on a real runner at PR time. This one does
49-
# not, because its live run reads the whole update window and would spend the
50-
# repository's shared request budget on every push to every branch. The half CI
51-
# can hold for free is the offline one, and it is held: `check:pm-board-snapshot`
52-
# runs the script's 70-case `--self-test` inside the required `Lint & Repo Gates`
53-
# job, which has no paths filter (`check:self-test-wired` is what requires that
54-
# step to exist). This job runs the same self-test again before it touches the
48+
# Every patrol in this tree exercises itself on the pull requests that change
49+
# it, and that is not a style preference: `check:pm-dispatch-gates` pins that NO
50+
# discovered gate family is reachable only through a schedule, so a workflow
51+
# whose script CI never runs at PR time reddens that gate by existing. A first
52+
# draft of this file omitted the trigger to keep PR-time cost at zero and was
53+
# refused by exactly that pin.
54+
#
55+
# So a pull_request run DOES execute the snapshot on a real runner — the
56+
# transport, the flags, the page walk and the rendering, proven where they will
57+
# actually run — and it writes NOTHING: no archive branch is checked out, the
58+
# run is `--dry-run` into the runner's temp dir, and the commit step is skipped.
59+
# It is also capped hard (`--limit`, `--max-requests` below), because the
60+
# repository's request budget is shared with the half-state patrol and the
61+
# closed-card sweep and must not be spent per push. ⛔ A pull request must never
62+
# write to the board OR to the archive.
63+
#
64+
# The offline half is held for free beside it: `check:pm-board-snapshot` runs the
65+
# script's 70-case `--self-test` inside the required `Lint & Repo Gates` job,
66+
# which has no paths filter (`check:self-test-wired` is what requires that step
67+
# to exist). This job runs the same self-test again before it touches the
5568
# archive, so a broken tool fails loudly instead of committing a broken backup.
5669
#
5770
# ## Adopting this in a sibling repo
@@ -80,6 +93,18 @@ on:
8093
# nothing cheaper than a webhook closes that, which is a different card.
8194
- cron: '37 1,7,13,19 * * *'
8295
workflow_dispatch: {}
96+
# Changes to the archiver itself get exercised before they merge. The paths
97+
# name every file the run actually loads — the archiver, the module it imports
98+
# its proxy plan and repo resolver from, the entry-guard helper both import,
99+
# and this file. An undeclared dependency is one a change can break without
100+
# this trigger firing, which is the same defect the adopt list above warns
101+
# about, one layer along.
102+
pull_request:
103+
paths:
104+
- 'scripts/pm/board-snapshot.mjs'
105+
- 'scripts/pm/check-half-states.mjs'
106+
- 'scripts/invoked-as.mjs'
107+
- '.github/workflows/board-snapshot.yml'
83108

84109
# Least privilege. `contents: write` is for the archive BRANCH and nothing else;
85110
# the two read scopes are what the snapshot reads. ⛔ This job never writes to
@@ -122,6 +147,7 @@ jobs:
122147
# checkout of it carries the archive and nothing else. First run creates it
123148
# locally; the push below is what publishes it.
124149
- name: Check out or create the archive branch
150+
if: github.event_name != 'pull_request'
125151
run: |
126152
set -euo pipefail
127153
git fetch --depth=1 origin '+refs/heads/board-archive:refs/remotes/origin/board-archive' || true
@@ -151,9 +177,15 @@ jobs:
151177
# in, always. A copy of this file cannot end up archiving the repo it
152178
# was copied FROM, and the script refuses to guess when this is unset.
153179
PM_SWEEP_REPO: ${{ github.repository }}
180+
# A pull_request run PROVES the tool on a runner and writes nothing:
181+
# the temp dir has no manifest, so it is a full read, capped to a
182+
# handful of numbers and a budget the shared quota will not notice.
183+
# ⛔ The two spellings differ in WHERE they write and in nothing else,
184+
# so what a PR exercises is the path a schedule takes.
185+
SNAPSHOT_ARGS: ${{ github.event_name == 'pull_request' && '--out=board --dry-run --limit=5 --max-requests=25' || '--out=archive/board' }}
154186
run: |
155187
set +e
156-
node scripts/pm/board-snapshot.mjs --out=archive/board \
188+
node scripts/pm/board-snapshot.mjs $SNAPSHOT_ARGS \
157189
> "$RUNNER_TEMP/snapshot.md" 2> "$RUNNER_TEMP/snapshot.err"
158190
code=$?
159191
set -e
@@ -171,6 +203,7 @@ jobs:
171203
# check never costs the records the same run archived.
172204
- name: Commit and push the archive
173205
id: commit
206+
if: github.event_name != 'pull_request'
174207
run: |
175208
set -euo pipefail
176209
if [ -z "$(git -C archive status --porcelain)" ]; then

‎scripts/pm/board-snapshot.mjs‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,10 @@ export function renderRun(result, options) {
809809

810810
const check = m.count_check;
811811
if (check.verdict === 'pending') {
812-
lines.push(` count check PENDING — the snapshot is incomplete, so its census is no reading about the board (expected ${check.expected}, archived ${check.archived}).`);
812+
const board = m.board.open_issues_count === null
813+
? 'the board\'s own count was not read this run'
814+
: `the board reports ${check.expected}`;
815+
lines.push(` count check PENDING — the snapshot is incomplete, so its census is no reading about the board (archived ${check.archived}; ${board}).`);
813816
} else if (check.ok) {
814817
lines.push(` count check ok — ${check.archived} open issue(s) archived, board says ${check.expected} (open_issues_count ${m.board.open_issues_count} minus ${m.board.open_pull_requests} open pull request(s)).`);
815818
} else if (check.verdict === 'shortfall') {
@@ -1336,7 +1339,12 @@ if (isEntrypoint(import.meta.url)) {
13361339
}
13371340
process.exit(code);
13381341
} else {
1339-
const rearmed = rearmThroughProxy(process.argv.slice(2));
1342+
// ⛔ --restore is not re-exec'd: it reads the archive directory and makes no
1343+
// request at all, so routing its transport would spawn a child to prove a
1344+
// route nothing in that mode uses. The absence of the re-exec line is part
1345+
// of what "prints, never posts, reads nothing but the directory" looks like.
1346+
const restoring = process.argv.some((a) => a === '--restore' || a.startsWith('--restore='));
1347+
const rearmed = restoring ? null : rearmThroughProxy(process.argv.slice(2));
13401348
if (rearmed !== null) process.exit(rearmed);
13411349
main(process.argv.slice(2)).then((code) => process.exit(code));
13421350
}

0 commit comments

Comments
 (0)