-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathplatform-checklist-watchdog.yml
More file actions
434 lines (413 loc) · 23.9 KB
/
Copy pathplatform-checklist-watchdog.yml
File metadata and controls
434 lines (413 loc) · 23.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
name: Platform-Checklist Watchdog
# The standing caller for `pnpm check:platform-checklist` (#11730).
#
# ## What this changes, and the one thing it deliberately does NOT
#
# The gate stays OUT of per-PR CI. That is a recorded maintainer decision, kept
# verbatim in the `lint.yml` NOTE beside the checklist gates and in
# `docs/qa/platform-checklist/README.md` under "Operating cadence": the
# checklist is a QA ledger, not a code gate, so an unrelated PR is never blocked
# by checklist drift. ⛔ This workflow does not touch that. Only the REPORTING
# CHANNEL changes — which is the whole of what was ruled.
#
# So this file carries `schedule:`, `workflow_dispatch:`, and a `pull_request:`
# trigger `paths`-filtered to THIS FILE AND NOTHING ELSE — the posture
# `half-state-patrol.yml`, `release-coverage-patrol.yml` and
# `required-set-patrol.yml` all keep, so a change to the patrol is exercised
# before it merges. ⛔ NO `merge_group:` and ⛔ NO `pull_request_target:`, ever.
#
# The filter is what makes that trigger compatible with the standing decision
# rather than a back door through it. The decision's own words, in
# `.github/workflows/lint.yml`: "keeping it out of the per-PR path means an
# unrelated PR is never blocked by checklist drift." A trigger that fires only
# on a pull request editing this workflow blocks no unrelated pull request at
# all — it preserves the decision's purpose while changing its letter, which is
# exactly what the maintainer ruled on 2026-09-06.
#
# ⛔ A `pull_request` run NEVER writes to the board. It runs the gate, publishes
# the output to the run summary, and stops there.
#
# ## Why a workflow, and not "a seat should run it"
#
# Measured, twice, not supposed. The gate went RED on `main` (a coverage.json /
# manifest UNCLASSIFIED reading) and NO lane turned red, because there was no
# channel: the red was found by a dev who happened to run the gate. Two further
# checklist cards had their devs run the gate on their own initiative and record
# the verdict honestly. Three for three is good discipline and says nothing at
# all about the mechanism — the gate's correctness was resting on somebody
# remembering, which is the shape a gate exists to replace. An alarm added to a
# script nobody runs is still silence.
#
# ## What lands where — and why this one is NOT a pinned anchor
#
# The other three patrols own ONE pinned anchor issue and rewrite its body every
# run, heartbeat included. This one files or refreshes an issue only when the
# gate is RED, and does nothing whatsoever on green. That is the ruled
# behaviour, and the difference is a real trade, stated plainly in both
# directions:
#
# * Their heartbeat is what tells a reader the patrol is still alive — a
# `Swept` timestamp that stops advancing past the cadence the same line
# states is how a dead caller is noticed (#17720: the cadence is rendered
# because 「stalled」 is undecidable without it, and the four patrols here
# differ by 4×). ON GREEN this workflow has no such tell at all, because it
# has no card, so a silent death on a green tree reads exactly like a clean
# checklist. What stands in for it: this job goes RED (and the Actions run
# list shows it) whenever the gate could not run or the report could not be
# delivered, and the run history itself is the record. A card that DOES
# stand carries the heartbeat and its deadline like the other three — with
# the one ambiguity this workflow's shape implies, stated in the card
# itself: a stalled `Swept` there means the caller died OR the gate went
# green, and nothing closes the card either way.
# * In exchange, a green tree has no standing issue to prune, and the card the
# devx seat grades is minted only when there is something to grade.
#
# ⛔ On green this job files nothing, edits nothing and CLOSES nothing — an open
# watchdog issue that has gone green is a triage judgement for the devx seat
# (which owns the filed cards), not a state for an unattended run to clear.
#
# ## De-duplication — read off `merge-queue-triage.yml`, whose lesson is negative
#
# The anchor lookup keys on an IDENTITY LABEL that is never graded off, plus a
# stable body marker, plus the exact title as a third way in. Each of those three
# is there because one of the others fails on its own:
#
# * ⛔ NOT `pm:queue` and ⛔ NOT `tooling`. Those are TRIAGE-STATE labels:
# first-touch grading takes `pm:queue` off by definition, and a lookup keyed
# on it stops seeing the anchor the moment somebody triages it — turning
# every further red into a fresh duplicate. `merge-queue-triage.yml` paid
# for that reading already and its header records it; this file inherits the
# fix rather than re-deriving it.
# * The TITLE is stable across refreshes — it names no failing item, no count
# and no date. A title carrying the first failing item would change with the
# failure and make the anchor unfindable by title the moment the checklist
# drifted somewhere else.
# * The BODY MARKER is plain text, ⛔ never an HTML comment: this platform's
# body sanitizer is measured to eat short angle-bracket fragments, and a
# de-dup key that can be silently swallowed is a de-dup key that files a
# duplicate a day. `half-state-patrol.yml`'s generated body uses the same
# plain-text marker idiom for the same reason.
#
# A CLOSED anchor is evidence too, and is not reopened: the gate going red again
# after the card was answered is a REGRESSION, so a new issue is filed and it
# names the one it regressed from. Reopening would bury that distinction.
on:
schedule:
# Daily. The gate is a ~1s offline read, so cadence is bounded by how long a
# red may sit unseen, not by cost; a day is the same order the sibling
# patrols settled on and is two orders better than the status quo (never).
#
# The minute is offset off the top of the hour ON PURPOSE — scheduled
# workflows queue behind everyone else's `:00` cron — and off the three
# sibling patrols' minutes, so two patrols never contend for the same runner
# minute.
#
# ⚠️ Two readers, one fact. This value is restated as `PATROL_SCHEDULE` in
# the `env:` block below, where the report step computes the deadline the
# card's own `Swept` line is judged against (#17720). Change the cron and
# change that env in the same edit, or the card publishes a deadline nobody
# schedules.
- cron: '51 2 * * *'
# The manual fire, and the smoke test this card exits on.
workflow_dispatch: {}
# ⛔ PATHS-FILTERED TO THIS ONE FILE, and that filter is the whole reason this
# trigger is allowed to exist. The maintainer's standing decision, in its own
# words in `.github/workflows/lint.yml`: "keeping it out of the per-PR path
# means an unrelated PR is never blocked by checklist drift." A trigger that
# fires ONLY on a pull request editing this workflow blocks no unrelated PR at
# all, so it preserves that decision's purpose while changing its letter — the
# posture every patrol in this repo already keeps, and the one
# `scripts/pm/dispatch-gates.mjs` pins tree-wide.
#
# ⛔ Do NOT widen this list. A second path here would put the checklist gate on
# the critical path of pull requests that have nothing to do with it, which IS
# the thing the decision forbids;
# `scripts/check-platform-checklist-watchdog.mjs` refuses any widening and its
# self-test proves that refusal fires.
#
# ⛔ And a pull_request run NEVER writes to the board — see the `if:` on the
# issue step. It proves the transport, the invocation and the rendering on a
# real runner, and nothing else.
pull_request:
paths:
- '.github/workflows/platform-checklist-watchdog.yml'
# ⛔ NO `merge_group:` and ⛔ NO `pull_request_target:` here, ever — those two
# are refused OUTRIGHT, because no `paths:` filter makes either safe in this
# workflow. `scripts/check-platform-checklist-watchdog.mjs` fails when either
# appears, and fails just as loudly when the `pull_request:` trigger above
# loses its filter or gains a second path; its self-test drives every one of
# those cases rather than asserting them into the void.
# Least privilege. The gate is read-only against the filesystem by construction
# (no socket, no token), and this job writes issues and nothing else — no label
# on anybody's PR, no state, no comment stream.
permissions:
contents: read
issues: write
# One watchdog at a time. A scheduled run overlapping a manual dispatch would
# have two runs racing to file the same anchor, and the loser would mint a
# duplicate.
concurrency:
group: platform-checklist-watchdog
cancel-in-progress: false
env:
# The anchor's IDENTITY — all three ways in, single-sourced here so the
# workflow and its static pin read the same strings.
#
# ⛔ The identity label is the machine's and is never graded off. It is NOT a
# triage-state label; see the header.
ANCHOR_IDENTITY_LABEL: platform-checklist-watchdog
ANCHOR_TITLE: 'check:platform-checklist is red on main'
ANCHOR_MARKER: os-platform-checklist-watchdog
# Applied on CREATE only, beside the identity label: the ruled routing for the
# cards this watchdog mints. Never rewritten on a refresh — grading is the
# devx seat's and a refresh must not undo it.
ANCHOR_ROUTING_LABELS: 'pm:queue,tooling'
# This workflow's own `cron:`, restated here so the report step can state the
# cadence that makes the card's `Swept` line decidable (#17720). The three
# sibling patrol anchors carry the same heartbeat reading and one of them
# fires every six hours, so a reader who carries a cadence over from a
# neighbour is wrong by 4× — which is why each anchor states its own rather
# than leaving the reader to go and read a workflow file.
#
# ⛔ A literal, not a parse of this file: the report body is inline
# `github-script` that no test reaches, so the cheapest honest route is the
# one with the least inline logic. The cost is the restatement above, and the
# comment at the `cron:` is what pays it.
PATROL_SCHEDULE: '51 2 * * *'
jobs:
watchdog:
# ⛔ This job is NOT a required context and must never become one. It runs on
# no pull request at all, so a ruleset naming it would block every PR
# forever on a check that cannot report.
name: Platform checklist gate sweep
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v7
# Kept as this job's own step rather than folded into the composite
# below: `scripts/check-node-version.mjs` scans `.github/workflows/*.yml`
# only and reports how many setup-node steps it audited, so a step moved
# out of sight would silently shrink its census.
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
# `pnpm` itself is the requirement, NOT the workspace. The gate is invoked
# through its PACKAGE SCRIPT (`pnpm check:platform-checklist`), which is a
# pinned property of this file — an inlined `node scripts/...` copy would
# drift from the script the maintainer's cadence documentation names, and
# would silently drop the `checklist-select --self-test` leg the alias runs
# first.
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm
# No `pnpm install`. Both halves of the alias import `node:` builtins and
# repo-local modules only — no npm dependency — so installing the
# workspace would buy nothing and would give an unattended daily run a
# lockfile it could fail on.
- name: Run the platform checklist gate
id: gate
run: |
set +e
pnpm check:platform-checklist \
> "$RUNNER_TEMP/gate.out" 2> "$RUNNER_TEMP/gate.err"
code=$?
set -e
# Captured with NO pipe in between. `cmd | tail` reports the PIPE's
# status — `tail` essentially never fails — so a red gate and a green
# one would both read as 0, and this exit code is the entire input to
# the decision below.
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
echo "pnpm check:platform-checklist exited $code"
- name: Publish the gate output to the run summary
# Always: on a green run this IS the whole product, and on a red one it
# makes the run log self-contained for whoever opens it from the issue.
if: always()
run: |
{
echo "### Platform-checklist watchdog — gate exit ${{ steps.gate.outputs.exit_code }}"
echo
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "_Board write skipped: a pull_request run proves the sweep without filing or refreshing anything._"
echo
fi
echo '```'
cat "$RUNNER_TEMP/gate.out" 2>/dev/null || echo '(no stdout captured)'
cat "$RUNNER_TEMP/gate.err" 2>/dev/null || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: File or refresh the watchdog issue
# RED only. On green this step does not run, and nothing else in this
# job writes anything — that is the ruled behaviour.
#
# ⛔ And NEVER on a `pull_request` run, whatever the gate said. A pull
# request proves the transport, the invocation and the rendering on a
# real runner; it must not touch the board. That is the posture every
# patrol in this repo keeps, and
# `scripts/check-platform-checklist-watchdog.mjs` refuses a board write
# whose `if:` lacks this guard.
if: steps.gate.outputs.exit_code != '0' && github.event_name != 'pull_request'
uses: actions/github-script@v9
env:
GATE_EXIT: ${{ steps.gate.outputs.exit_code }}
with:
# Delivery is retried, never assumed: this write is the entire product
# of a red run, and a transient answer from the issues endpoint would
# otherwise discard a completed sweep.
retries: 3
script: |
const fs = require('fs');
const path = require('path');
const owner = context.repo.owner;
const repo = context.repo.repo;
const marker = process.env.ANCHOR_MARKER;
const title = process.env.ANCHOR_TITLE;
const identityLabel = process.env.ANCHOR_IDENTITY_LABEL;
const routingLabels = process.env.ANCHOR_ROUTING_LABELS
.split(',').map((s) => s.trim()).filter(Boolean);
const gateExit = process.env.GATE_EXIT;
const runUrl = `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const read = (name) => {
try { return fs.readFileSync(path.join(process.env.RUNNER_TEMP, name), 'utf8'); }
catch { return ''; }
};
// The gate's OWN output, verbatim, both streams — the ruling asks
// for the gate's output and this workflow does not re-word it.
const output = [read('gate.out'), read('gate.err')]
.filter((s) => s.trim()).join('\n').trimEnd();
if (!output.trim()) {
// A red gate that printed nothing is not a finding anybody can
// act on, and filing it would mint an empty card that reads like
// a judged one. Fail instead: the last step turns the run red.
throw new Error(`the gate exited ${gateExit} but produced no output on either stream — refusing to file an empty card`);
}
// ── Anchor lookup ────────────────────────────────────────────────
// Three ways in, none of them sufficient alone; see this file's
// header. `pull_request` excludes PRs, which the issues endpoint
// returns alongside issues.
const isThisAnchor = (i) => !i.pull_request
&& (String(i.body ?? '').includes(marker) || i.title === title);
const MAX_PAGES = 3;
const candidates = [];
let scanComplete = true;
for (let page = 1; page <= MAX_PAGES; page += 1) {
const res = await github.rest.issues.listForRepo({
owner, repo, state: 'all', labels: identityLabel,
sort: 'created', direction: 'desc', per_page: 100, page,
});
candidates.push(...res.data.filter(isThisAnchor));
if (res.data.length < 100) break;
if (page === MAX_PAGES) scanComplete = false;
}
if (!scanComplete) {
// A truncated scan cannot establish ABSENCE, and filing on an
// unestablished absence is how a watchdog mints a duplicate a
// day. Refusing is the safe direction: the finding is still in
// this run's summary and the next run retries.
throw new Error(`the anchor scan hit its ${MAX_PAGES}-page bound without completing — absence is NOT established, so nothing was filed. The gate output is in this run's summary.`);
}
// Resolution order: an OPEN anchor wins, and the OLDEST open one
// wins among those — that is the issue any duplicates were closed
// against.
const open = candidates.filter((i) => i.state !== 'closed')
.sort((a, b) => a.number - b.number);
const closed = candidates.filter((i) => i.state === 'closed')
.sort((a, b) => b.number - a.number);
const existing = open[0] ?? null;
// A closed anchor that was not closed as a duplicate is an ANSWERED
// one: the gate is red again, which is a regression rather than a
// continuation, and the new card has to say which issue it
// regressed from or the previous answer is lost.
const priorAnchor = !existing
? (closed.find((i) => i.state_reason !== 'duplicate') ?? null)
: null;
// The cadence that makes the `Swept` line below decidable (#17720).
// `PATROL_SCHEDULE` is this workflow's own `cron:`, restated in the
// `env:` block beside the other single-sourced card strings.
// ⛔ Only the daily `M H * * *` form is computed; anything else says
// so in the body rather than publishing a deadline derived from a
// schedule this did not read.
const sweptAt = new Date();
const schedule = String(process.env.PATROL_SCHEDULE ?? '').trim();
const cronFields = /^(\d{1,2}) (\d{1,2}) \* \* \*$/.exec(schedule);
let expectation = `⚠️ expected cadence UNSTATED — PATROL_SCHEDULE is \`${schedule}\`, which is not the daily \`M H * * *\` form this step computes a deadline from`;
if (cronFields && Number(cronFields[1]) <= 59 && Number(cronFields[2]) <= 23) {
const next = new Date(sweptAt);
next.setUTCHours(Number(cronFields[2]), Number(cronFields[1]), 0, 0);
if (next <= sweptAt) next.setUTCDate(next.getUTCDate() + 1);
expectation = `expected every 24h while this card stands (cron \`${schedule}\` UTC) · next by ${next.toISOString().slice(0, 16)}Z`;
}
const body = [
`${marker} — machine-findable marker for this generated view. ⛔ Do not delete this line: it is how the watchdog finds this card instead of filing a new one every day.`,
'',
`# \`check:platform-checklist\` is RED on \`main\``,
'',
`_Swept ${sweptAt.toISOString()} · ${expectation} · [run log](${runUrl}) · commit \`${process.env.GITHUB_SHA}\` · trigger \`${context.eventName}\` · gate exit ${gateExit}._`,
'',
'The `Swept` line above is this watchdog\'s heartbeat, and it states the cadence that makes',
'「stalled」 decidable: while the gate stays red this card is refreshed on that schedule, so a',
'timestamp still sitting there past the `next by` deadline means either the standing caller',
'died or the gate went green — this watchdog files nothing and closes nothing on green, so',
'both readings end at this card. ⛔ Do not carry a cadence over from a sibling patrol anchor:',
'they differ by up to 4× and each states its own.',
'',
'The platform test checklist gate is red. It is **not** wired into per-PR CI (a standing',
'maintainer decision — the checklist is a QA ledger, not a code gate), so this card is the',
'channel that sees the red. Nothing is blocked by it.',
'',
'⛔ The remedy is **never** to edit the checklist data to make the gate green. Read the',
'output below, fix what it names, and re-run `pnpm check:platform-checklist` locally.',
'',
...(priorAnchor
? [
`⚠️ The previous watchdog card #${priorAnchor.number} was closed on its own merits (not as a`,
'duplicate) and the gate is red again ⇒ this is a **regression**; the earlier conclusion is',
'in that issue.',
'',
]
: []),
"The gate's own output, verbatim:",
'',
'```',
output,
'```',
].join('\n');
if (existing) {
// Rewritten in place, never a comment per run: a per-run comment
// stream is a second tracker nobody prunes, and this platform's
// edit history is already the archive. ⛔ Labels are NOT rewritten
// — grading is the devx seat's and a refresh must not undo it.
await github.rest.issues.update({
owner, repo, issue_number: existing.number, body,
});
core.info(`refreshed watchdog anchor #${existing.number} (${body.length} chars, gate exit ${gateExit})`);
core.notice(`check:platform-checklist is red on main — watchdog card #${existing.number} refreshed.`);
return;
}
const created = await github.rest.issues.create({
owner, repo, title, body,
// Additive on create; ⛔ nothing here ever replaces a whole label
// set (`check:whole-set-label-write` refuses that verb outright).
labels: [identityLabel, ...routingLabels],
});
core.info(`filed watchdog anchor #${created.data.number} (${body.length} chars, gate exit ${gateExit})`);
core.notice(`check:platform-checklist is red on main — watchdog card #${created.data.number} filed.`);
- name: Fail the run if the sweep left no reading
# LAST, on purpose: on a red gate the card is filed or refreshed BEFORE
# this runs, so the run that raises an alarm has already landed the
# truth.
#
# ⛔ A RED GATE is not a failure condition and never reaches here — a
# red gate is a SUCCESSFUL patrol, and a job that went red every day the
# checklist was red would teach everyone to skim it, which is the
# failure this whole card is about. Delivery failure needs no step
# either: the github-script above throws, and a throw fails that step.
#
# What is left is the one reading that must never look green: a sweep
# step that finished without recording an exit code at all. Nothing
# downstream could tell that apart from `exit_code=0`, and "the gate was
# not read" must never render as "the checklist is clean".
if: always() && steps.gate.outputs.exit_code == ''
run: |
echo "the gate step recorded no exit code, so this run is NOT a reading about the checklist -- it is neither a clean one nor a red one. See this run's summary."
exit 1