Skip to content

Commit e9419c3

Browse files
committed
fix(pm): every patrol anchor states the cadence that makes a stalled Swept line decidable
Four patrol writers stamp a `Swept <t>` line and tell the reader that a line which stops advancing means the standing caller died. None stated the interval that makes "stalled" decidable, and the four cadences differ by 4x, so a reader who carries one over from a neighbouring anchor is wrong in both directions. Each writer now renders the schedule, the interval and the next-expected deadline beside its own `Swept` stamp, and the heartbeat sentence points at that deadline instead of asking for a judgement it withheld the input for. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK
1 parent e3411ed commit e9419c3

5 files changed

Lines changed: 339 additions & 16 deletions

File tree

.github/workflows/platform-checklist-watchdog.yml

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,18 @@ name: Platform-Checklist Watchdog
4848
# directions:
4949
#
5050
# * Their heartbeat is what tells a reader the patrol is still alive — a
51-
# `Swept` timestamp that stops advancing is how a dead caller is noticed.
52-
# This workflow has no such tell, so a silent death here reads exactly like
53-
# a clean checklist. What stands in for it: this job goes RED (and the
54-
# Actions run list shows it) whenever the gate could not run or the report
55-
# could not be delivered, and the run history itself is the record.
51+
# `Swept` timestamp that stops advancing past the cadence the same line
52+
# states is how a dead caller is noticed (#17720: the cadence is rendered
53+
# because 「stalled」 is undecidable without it, and the four patrols here
54+
# differ by 4×). ON GREEN this workflow has no such tell at all, because it
55+
# has no card, so a silent death on a green tree reads exactly like a clean
56+
# checklist. What stands in for it: this job goes RED (and the Actions run
57+
# list shows it) whenever the gate could not run or the report could not be
58+
# delivered, and the run history itself is the record. A card that DOES
59+
# stand carries the heartbeat and its deadline like the other three — with
60+
# the one ambiguity this workflow's shape implies, stated in the card
61+
# itself: a stalled `Swept` there means the caller died OR the gate went
62+
# green, and nothing closes the card either way.
5663
# * In exchange, a green tree has no standing issue to prune, and the card the
5764
# devx seat grades is minted only when there is something to grade.
5865
#
@@ -96,6 +103,12 @@ on:
96103
# workflows queue behind everyone else's `:00` cron — and off the three
97104
# sibling patrols' minutes, so two patrols never contend for the same runner
98105
# minute.
106+
#
107+
# ⚠️ Two readers, one fact. This value is restated as `PATROL_SCHEDULE` in
108+
# the `env:` block below, where the report step computes the deadline the
109+
# card's own `Swept` line is judged against (#17720). Change the cron and
110+
# change that env in the same edit, or the card publishes a deadline nobody
111+
# schedules.
99112
- cron: '51 2 * * *'
100113
# The manual fire, and the smoke test this card exits on.
101114
workflow_dispatch: {}
@@ -154,6 +167,18 @@ env:
154167
# cards this watchdog mints. Never rewritten on a refresh — grading is the
155168
# devx seat's and a refresh must not undo it.
156169
ANCHOR_ROUTING_LABELS: 'pm:queue,tooling'
170+
# This workflow's own `cron:`, restated here so the report step can state the
171+
# cadence that makes the card's `Swept` line decidable (#17720). The three
172+
# sibling patrol anchors carry the same heartbeat reading and one of them
173+
# fires every six hours, so a reader who carries a cadence over from a
174+
# neighbour is wrong by 4× — which is why each anchor states its own rather
175+
# than leaving the reader to go and read a workflow file.
176+
#
177+
# ⛔ A literal, not a parse of this file: the report body is inline
178+
# `github-script` that no test reaches, so the cheapest honest route is the
179+
# one with the least inline logic. The cost is the restatement above, and the
180+
# comment at the `cron:` is what pays it.
181+
PATROL_SCHEDULE: '51 2 * * *'
157182

158183
jobs:
159184
watchdog:
@@ -313,12 +338,36 @@ jobs:
313338
? (closed.find((i) => i.state_reason !== 'duplicate') ?? null)
314339
: null;
315340
341+
// The cadence that makes the `Swept` line below decidable (#17720).
342+
// `PATROL_SCHEDULE` is this workflow's own `cron:`, restated in the
343+
// `env:` block beside the other single-sourced card strings.
344+
// ⛔ Only the daily `M H * * *` form is computed; anything else says
345+
// so in the body rather than publishing a deadline derived from a
346+
// schedule this did not read.
347+
const sweptAt = new Date();
348+
const schedule = String(process.env.PATROL_SCHEDULE ?? '').trim();
349+
const cronFields = /^(\d{1,2}) (\d{1,2}) \* \* \*$/.exec(schedule);
350+
let expectation = `⚠️ expected cadence UNSTATED — PATROL_SCHEDULE is \`${schedule}\`, which is not the daily \`M H * * *\` form this step computes a deadline from`;
351+
if (cronFields && Number(cronFields[1]) <= 59 && Number(cronFields[2]) <= 23) {
352+
const next = new Date(sweptAt);
353+
next.setUTCHours(Number(cronFields[2]), Number(cronFields[1]), 0, 0);
354+
if (next <= sweptAt) next.setUTCDate(next.getUTCDate() + 1);
355+
expectation = `expected every 24h while this card stands (cron \`${schedule}\` UTC) · next by ${next.toISOString().slice(0, 16)}Z`;
356+
}
357+
316358
const body = [
317359
`${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.`,
318360
'',
319361
`# \`check:platform-checklist\` is RED on \`main\``,
320362
'',
321-
`_Swept ${new Date().toISOString()} · [run log](${runUrl}) · commit \`${process.env.GITHUB_SHA}\` · trigger \`${context.eventName}\` · gate exit ${gateExit}._`,
363+
`_Swept ${sweptAt.toISOString()} · ${expectation} · [run log](${runUrl}) · commit \`${process.env.GITHUB_SHA}\` · trigger \`${context.eventName}\` · gate exit ${gateExit}._`,
364+
'',
365+
'The `Swept` line above is this watchdog\'s heartbeat, and it states the cadence that makes',
366+
'「stalled」 decidable: while the gate stays red this card is refreshed on that schedule, so a',
367+
'timestamp still sitting there past the `next by` deadline means either the standing caller',
368+
'died or the gate went green — this watchdog files nothing and closes nothing on green, so',
369+
'both readings end at this card. ⛔ Do not carry a cadence over from a sibling patrol anchor:',
370+
'they differ by up to 4× and each states its own.',
322371
'',
323372
'The platform test checklist gate is red. It is **not** wired into per-PR CI (a standing',
324373
'maintainer decision — the checklist is a QA ledger, not a code gate), so this card is the',

.github/workflows/release-coverage-patrol.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ on:
102102
# half-state-patrol.yml's `37 1,7,13,19` so two patrols never contend for the
103103
# same runner minute.
104104
schedule:
105+
# ⚠️ Two readers, one fact. This value is also handed to the renderer as
106+
# `PATROL_SCHEDULE` (the "Render the report" step below) so the anchor body
107+
# can state the deadline its own heartbeat line is judged against (#17720).
108+
# Change the cron and change that env in the same edit, or the anchor
109+
# publishes a deadline nobody schedules.
105110
- cron: '19 4 * * *'
106111
workflow_dispatch: {}
107112
# Changes to the patrol itself get exercised before they merge — the same
@@ -238,6 +243,14 @@ jobs:
238243
env:
239244
ADVISORY_CODE: ${{ steps.sweep.outputs.advisory_code }}
240245
STRICT_CODE: ${{ steps.sweep.outputs.strict_code }}
246+
# This workflow's own `cron:`, handed to the renderer so the anchor
247+
# body states the cadence that makes its heartbeat line decidable
248+
# (#17720). It lives HERE rather than in the script because the
249+
# schedule is declared here: a second copy in the renderer would be a
250+
# second place to drift from, and a drifted deadline reads exactly
251+
# like a correct one. ⛔ Unset, the renderer says so in the body
252+
# rather than assuming a period — it never guesses.
253+
PATROL_SCHEDULE: '19 4 * * *'
241254
PROVENANCE: >-
242255
run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
243256
· commit `${{ github.sha }}` · trigger `${{ github.event_name }}`

.github/workflows/test-nightly-tiers.yml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ on:
7878
# patrol's minute, so two of them never contend for the same runner minute;
7979
# 05:29 sits after `rerun-safety-nightly` (04:00) and `coverage-nightly`
8080
# (05:00) have started and well before `showcase-smoke` (07:00).
81+
#
82+
# ⚠️ Two readers, one fact. This value is restated as `PATROL_SCHEDULE` in
83+
# the `env:` block below, where the report step computes the deadline the
84+
# card's own `Swept` line is judged against (#17720). Change the cron and
85+
# change that env in the same edit, or the card publishes a deadline nobody
86+
# schedules.
8187
- cron: '29 5 * * *'
8288
workflow_dispatch: {}
8389
# ⛔ PATHS-FILTERED TO THIS FILE AND THE SWITCH READER IT INVOKES, and that
@@ -121,6 +127,18 @@ env:
121127
CARD_MARKER: os-nightly-tiers
122128
# Applied on CREATE only, additively; never rewritten on a refresh.
123129
CARD_LABELS: 'bug,domain:devx,priority:p1'
130+
# This workflow's own `cron:`, restated here so the report step can state the
131+
# cadence that makes the card's `Swept` line decidable (#17720). The sibling
132+
# patrol anchors carry the same heartbeat reading and one of them fires every
133+
# six hours, so a reader who carries a cadence over from a neighbour is wrong
134+
# by 4× — which is why each card states its own rather than leaving the
135+
# reader to go and read a workflow file.
136+
#
137+
# ⛔ A literal, not a parse of this file: the report body is inline
138+
# `github-script` that no test reaches, so the cheapest honest route is the
139+
# one with the least inline logic. The cost is the restatement above, and the
140+
# comment at the `cron:` is what pays it.
141+
PATROL_SCHEDULE: '29 5 * * *'
124142

125143
jobs:
126144
tiers:
@@ -440,6 +458,23 @@ jobs:
440458
'',
441459
]);
442460
461+
// The cadence that makes the `Swept` line below decidable (#17720).
462+
// `PATROL_SCHEDULE` is this workflow's own `cron:`, restated in the
463+
// `env:` block beside the other single-sourced card strings.
464+
// ⛔ Only the daily `M H * * *` form is computed; anything else says
465+
// so in the body rather than publishing a deadline derived from a
466+
// schedule this did not read.
467+
const sweptAt = new Date();
468+
const schedule = String(process.env.PATROL_SCHEDULE ?? '').trim();
469+
const cronFields = /^(\d{1,2}) (\d{1,2}) \* \* \*$/.exec(schedule);
470+
let expectation = `⚠️ expected cadence UNSTATED — PATROL_SCHEDULE is \`${schedule}\`, which is not the daily \`M H * * *\` form this step computes a deadline from`;
471+
if (cronFields && Number(cronFields[1]) <= 59 && Number(cronFields[2]) <= 23) {
472+
const next = new Date(sweptAt);
473+
next.setUTCHours(Number(cronFields[2]), Number(cronFields[1]), 0, 0);
474+
if (next <= sweptAt) next.setUTCDate(next.getUTCDate() + 1);
475+
expectation = `expected every 24h while this card stands (cron \`${schedule}\` UTC) · next by ${next.toISOString().slice(0, 16)}Z`;
476+
}
477+
443478
const filesSection = failing.length > 0
444479
? ['## Failing files', '', ...failing.map((f) => `- \`${f}\``), '']
445480
: extractorExit !== '0'
@@ -451,7 +486,14 @@ jobs:
451486
'',
452487
`# ${title}`,
453488
'',
454-
`_Swept ${new Date().toISOString()} · [run log](${runUrl}) · commit \`${process.env.GITHUB_SHA}\` · trigger \`${context.eventName}\` · tiers job result \`${result}\`._`,
489+
`_Swept ${sweptAt.toISOString()} · ${expectation} · [run log](${runUrl}) · commit \`${process.env.GITHUB_SHA}\` · trigger \`${context.eventName}\` · tiers job result \`${result}\`._`,
490+
'',
491+
'The `Swept` line above is this nightly\'s heartbeat, and it states the cadence that makes',
492+
'「stalled」 decidable: while the tiers stay red this card is refreshed on that schedule, so a',
493+
'timestamp still sitting there past the `next by` deadline means either the standing caller',
494+
'died or the nightly went green — nothing here files or closes anything on green, so both',
495+
'readings end at this card. ⛔ Do not carry a cadence over from a sibling patrol anchor: they',
496+
'differ by up to 4× and each states its own.',
455497
'',
456498
'The `e2e` and `live` test tiers — the files named `*.e2e.test.*` and `*.live.test.*` — run here',
457499
'nightly on `main` under `OS_TEST_TIERS=nightly` and nowhere else (the per-PR and merge-queue',

0 commit comments

Comments
 (0)