Repository navigation
1427 lines (1384 loc) · 80.8 KB
/
Copy pathfleet-patrols.yml
File metadata and controls
1427 lines (1384 loc) · 80.8 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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Fleet Patrols
# Four daily patrols, one file — ruling: batch #397 item 2 · letter A, the
# maintainer's 「同意」, recorded on the decision card that measured the fleet's
# scheduled workflows. Each job below is one retired workflow moved here whole:
# its steps, its `permissions:` grant, its `concurrency:` group and its `env:`
# are the retired file's, so the job's failure signal, its least-privilege
# grant and its budget are still its own, and nothing is widened to the
# workflow level.
#
# job formerly cron
# required-set-patrol required-set-patrol.yml 23 4,16 * * *
# platform-checklist-watchdog platform-checklist-watchdog.yml 51 2 * * *
# release-coverage-patrol release-coverage-patrol.yml 19 4 * * *
# board-snapshot board-snapshot.yml 7 2,8,14,20 * * *
#
# ## One file, four clocks
#
# A `schedule` event carries the cron expression that fired it
# (`github.event.schedule`), and every job's `if:` compares against its own, so
# a scheduled run executes exactly one job and the other three read as skipped.
# ⛔ A cron string under `schedule:` and the same string in its job's `if:` are
# ONE fact spelled twice: change both in the same edit. A cron no `if:` names
# fires a run that does nothing, and a job whose `if:` names a cron the
# schedule does not declare never runs — clause 7 of the watchdog pin refuses both.
#
# `workflow_dispatch` takes one input, `patrol`: `all` (the default) runs every
# job, a job id runs that one alone — the manual fire and the smoke test each
# retired workflow offered, without spending the board-snapshot's request budget
# to smoke-test the watchdog.
#
# ## The `pull_request` trigger, and what it is allowed to do
#
# Every patrol in this tree exercises itself on the pull requests that change it
# (`scripts/pm/dispatch-gates.mjs` pins tree-wide that no discovered gate family
# is reachable only through a schedule), so this file's `pull_request:` trigger
# is `paths`-filtered to the UNION of what the four retired files declared: this
# file, plus each job's own inputs. A matching pull request runs all four jobs.
# A `pull_request` run proves transport, invocation and rendering on a real
# runner and WRITES NOTHING — no job files, refreshes or archives anything on a
# pull_request event: every board write below carries that guard in its own
# `if:`, and `scripts/check-platform-checklist-watchdog.mjs` refuses one that
# does not. That pin also freezes this path list: widen it by one entry and the
# pin goes red, so the checklist gate cannot drift onto the per-PR path by
# accident. The standing decision that keeps `check:platform-checklist` out of
# per-PR CI — "an unrelated PR is never blocked by checklist drift" — holds in
# substance: no job here publishes a required context, and the watchdog job
# never reds on drift.
#
# ⛔ NO `merge_group:` and ⛔ NO `pull_request_target:`, ever — refused outright
# by the same pin; `scripts/check-required-contexts.mjs` asserts from its side
# that this file declares no `merge_group:` (a workflow without one deadlocks
# the queue if it is ever required-ized) and that no REQUIRED_CONTEXTS row
# names it.
#
# ## Where the retired files' history went
#
# GitHub keeps the runs of a deleted workflow file: the Actions sidebar lists
# each retired workflow by name while runs of it exist, and the workflow runs
# API still answers for the retired file (its workflow reads `state: deleted`).
# New history lands under "Fleet Patrols", one job per patrol — open a run and
# read its job list, or filter the run list by job name, to follow one patrol.
on:
schedule:
# ── `required-set-patrol` ─────────────────────────────────────────────────────────
# Twice a day, twelve hours apart, at :23 past the hour.
#
# The live required set is changed by hand, by the maintainer, a few times a
# month at most — detection latency of half a day is already two orders of
# magnitude better than the status quo (never), and the #5617 incident it
# guards against sat undetected for days. The minute is offset off the top
# of the hour on purpose: scheduled workflows queue behind everyone else's
# :00 cron, and this sweep shares the core API quota with the loop's hot
# path.
- cron: '23 4,16 * * *'
# ── `platform-checklist-watchdog` ─────────────────────────────────────────────────────────
# 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 * * *'
# ── `release-coverage-patrol` ─────────────────────────────────────────────────────────
# Nightly. #10268's acceptance criterion is "visible within a day of a minor
# publishing", so the cadence is a day, not a week. Offset from
# half-state-patrol.yml's `37 1,7,13,19` so two patrols never contend for the
# same runner minute.
#
# ⚠️ Two readers, one fact. This value is also handed to the renderer as
# `PATROL_SCHEDULE` (the "Render the report" step below) so the anchor body
# can state the deadline its own heartbeat line is judged against (#17720).
# Change the cron and change that env in the same edit, or the anchor
# publishes a deadline nobody schedules.
- cron: '19 4 * * *'
# ── `board-snapshot` ─────────────────────────────────────────────────────────
# Four times a day, six hours apart, at :07 past the hour. The HOURS are
# chosen against the other standing spenders listed above, not for tidiness.
#
# This job first shipped at `37 1,7,13,19` — the half-state patrol's cron to
# the minute. Both are scheduled board readers, both authenticate as this
# repository's `GITHUB_TOKEN`, and that budget is per repository per hour, so
# a snapshot run spending its full 800 leaves the patrol the remainder of the
# same window. Measured on the day this moved: the 13:37Z patrol run and the
# 13:41Z snapshot run were BOTH green, so nothing had been starved yet — the
# offset is PREVENTION, taken while the archive was still small enough that
# no run had yet spent its whole cap against the patrol's window.
#
# Of the five hour sets that share no hour with the patrol's 1,7,13,19, this
# is the least contended on this repository's cron inventory: 08, 14 and 20
# UTC carry no other scheduled workflow at all, and 02 UTC carries only the
# platform-checklist watchdog at :51 — 44 minutes after this run starts —
# plus CodeQL on Mondays. `4,10,16,22` was the obvious alternative and is
# worse on the same measurement: it drops an 800-request run into the busiest
# scheduled hour on this board (04:19 release-coverage patrol, 04:23
# required-set patrol, 04:37 branch reaper on Mondays, 04:41 create-smoke)
# and shares hour 16 with the required-set patrol.
#
# The minute is off the top of the hour on purpose, as in every patrol here:
# scheduled workflows queue behind everyone else's :00 cron.
#
# Six-hourly is the loss window this accepts: a card created and destroyed
# inside one interval was never archived, and nothing cheaper than a webhook
# closes that, which is a different card.
- cron: '7 2,8,14,20 * * *'
# The manual fire, and the smoke test. One input: `all` runs every job, a job
# id runs that one alone (each job's `if:` reads it).
workflow_dispatch:
inputs:
patrol:
description: 'Which patrol to run by hand: all, or one job id'
type: choice
default: all
options:
- all
- required-set-patrol
- platform-checklist-watchdog
- release-coverage-patrol
- board-snapshot
# Changes to a patrol, or to the sweep it calls, get exercised before they
# merge: the transport, the flags, the exit-code split and the rendering are
# proven on a real runner rather than argued about. No job publishes a
# required context, so a red here blocks nothing — and a pull_request run
# never writes the board or the archive (see the header).
#
# ⛔ Do NOT widen this list by hand. It is the union of the four retired
# files' own filters, frozen as a SET in
# `scripts/check-platform-checklist-watchdog.mjs`: a new entry is a reviewed
# edit to that pin in the same PR, never a quiet addition here.
pull_request:
paths:
- '.github/workflows/fleet-patrols.yml'
# `required-set-patrol`
- 'scripts/check-required-contexts.mjs'
# `release-coverage-patrol`
- 'scripts/check-release-section-coverage.mjs'
# `board-snapshot` — every file the run actually loads: the archiver, the
# module it imports its proxy plan and repo resolver from, and the
# entry-guard helper both import. An undeclared dependency is one a change
# can break without this trigger firing.
- 'scripts/pm/board-snapshot.mjs'
- 'scripts/pm/check-half-states.mjs'
- 'scripts/invoked-as.mjs'
# `platform-checklist-watchdog` names no path beyond this file: its gate
# runs through `pnpm check:platform-checklist`, which stays OFF the per-PR
# path by a standing maintainer decision.
jobs:
# ══ `required-set-patrol` — formerly `.github/workflows/required-set-patrol.yml` ══
#
# The standing caller for
# `node scripts/check-required-contexts.mjs --verify-required-set` (#9678).
#
# ## Why a workflow, and not "a seat should run it"
#
# `--verify-required-set` (#9642) diffs this repo's REQUIRED_CONTEXTS registry
# against the LIVE ruleset's required set, in both directions:
#
# A. a registry row whose context is absent from the live set — the gate
# family that row names is advisory RIGHT NOW, with no signal anywhere.
# #5617 verbatim: PR #5584 merged with its gate-carrying job red for 19
# minutes because that job was not in the required set at all.
# B. a live required context with no registry row — renaming that job detaches
# its gate silently, which is the defect check-required-contexts exists for.
#
# It shipped with no standing caller, so the only thing CI ran was its OFFLINE
# self-test: the mechanism existed and measured nothing. A sweep whose value is
# realized only when something runs it on a schedule, with nothing on a
# schedule, is the `check-half-states.mjs` shape verbatim — an alarm added to a
# script nobody runs is still silence, and half-state-patrol.yml exists because
# that cost a shift's worth of unnoticed findings.
#
# "Some seat should run it" also keeps not happening for a MEASURED reason
# rather than a discipline one. Re-measured 2026-08-19 from a dev session
# container, both spellings the script documents:
#
# node scripts/check-required-contexts.mjs --verify-required-set
# -> NOT VERIFIED, GET /repos/... answered HTTP 401 (exit 2)
# NODE_OPTIONS=--use-env-proxy node ... --verify-required-set
# -> NOT VERIFIED, GET /repos/... answered HTTP 403 (exit 2)
#
# i.e. the documented proxy remedy is not enough: the seat's egress policy
# refuses api.github.com outright. The seats that would notice drift are exactly
# the ones that cannot measure it, so the caller had to move somewhere the
# transport prerequisite is actually met. A GitHub Actions runner reaches the
# API directly with the workflow's own GITHUB_TOKEN — no proxy env, no
# `--use-env-proxy`; the read's whole price is `metadata=read`, which every
# token carries and none can give up.
#
# ## Where the report lands, and why nowhere else
#
# The run log and the run's STEP SUMMARY, and nothing else. Deliberately not an
# issue:
#
# - a NEW tracker is refused outright — this repo has one board, and a per-run
# comment stream or a fresh anchor is a second one that nobody prunes;
# - the EXISTING anchor (#9857) is owned end-to-end by half-state-patrol.yml,
# whose generator rewrites that body whole every run. A second writer would
# make each patrol silently discard the other's findings.
#
# What that costs is honestly stated: a completed sweep that FINDS drift exits 0
# and shows a green check in the Actions list, so the drift is one click deep.
# The `::warning::` annotation below is what keeps it from being invisible —
# annotations render on the run list itself. Routing findings to a durable
# surface (the PM round report, a rotating anchor) is a governed-surface change
# and is not this workflow's to make.
#
# ## Report-only, and the one thing that is NOT report-only
#
# ⛔ This workflow is NEVER a required context and must never become one.
# That is structural, not squeamish: the settings half of any required-set
# change is maintainer-only and lands AFTER the merge (the #9325 two-step), so a
# blocking version would be red on precisely the PR carrying the repo half and
# could not go green before merging — it would deadlock the sitting it claims to
# protect. The script's own header argues this at length, and
# check-required-contexts' self-test pins it from this side too: it asserts that
# this is the ONLY workflow running the flag, that this file declares no
# `merge_group:` trigger (a workflow without one deadlocks the queue the moment
# it is required-ized — assertion 6 of the pin), and that no REQUIRED_CONTEXTS
# row names this file.
#
# So FINDINGS never fail anything: a completed sweep exits 0 whether it found 0
# or 40 disagreements, and this job never writes a label, an issue or a state.
#
# The job DOES fail when the sweep could not RUN, or when its report could not
# be DELIVERED. That is not a gate on the repo; it is the patrol reporting its
# own death. `NOT VERIFIED` is not a pass and not a failure of the tree (#4690):
# exit 2 classifies the ENVIRONMENT, and a patrol that quietly stops reading
# would leave a run history of green checks that reads exactly like a clean
# required set. Failing costs nobody a PR — this workflow gates no branch and
# blocks no queue.
required-set-patrol:
# ⛔ This name is NOT a required context and must never be added to the
# ruleset — see the header. It is deliberately not registered in
# REQUIRED_CONTEXTS, and check-required-contexts' self-test asserts so.
name: Live required-set sweep
# One file, four clocks: a `schedule` event carries the cron that fired it, so
# each job runs only on its own cron; `workflow_dispatch` runs the one the input
# names (default: all four); a `pull_request` run exercises every job.
if: >-
(github.event_name != 'schedule' || github.event.schedule == '23 4,16 * * *')
&& (github.event_name != 'workflow_dispatch' || inputs.patrol == 'all' || inputs.patrol == 'required-set-patrol')
runs-on: ubuntu-latest
timeout-minutes: 10
# Least privilege. The sweep is read-only against the API by construction and
# this job writes no issue, no label and no comment, so `contents: read` is the
# whole grant — the ruleset read's documented price is `metadata=read`, which is
# below anything a workflow can toggle.
permissions:
contents: read
# One patrol at a time; a scheduled run overlapping a manual dispatch would burn
# two readings of the same set for one report.
concurrency:
group: required-set-patrol-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: false
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
# No `pnpm install`: the live mode imports nothing outside node: builtins
# and global fetch. Installing the workspace would buy nothing and would
# give a scheduled patrol a lockfile it could fail on.
#
# No NODE_OPTIONS=--use-env-proxy either, and that is not an omission: a
# runner reaches api.github.com directly. The flag exists for agent
# session containers, where it is necessary but (measured, #9678) still
# not sufficient — that is why this caller lives here and not there.
- name: Run the live required-set sweep
id: sweep
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set +e
node scripts/check-required-contexts.mjs --verify-required-set \
> "$RUNNER_TEMP/required-set.md" 2> "$RUNNER_TEMP/required-set.err"
code=$?
set -e
# Captured with NO pipe in between. `cmd | tail` would report the
# PIPE's status — `tail` essentially never fails, so a swept run and
# an unreadable one both read as 0, and the exit split (0 swept / 2
# environment) is the entire classification this patrol delivers.
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
echo "check-required-contexts --verify-required-set exited $code"
cat "$RUNNER_TEMP/required-set.err" >&2 || true
- name: Publish the sweep to the run summary
# Always: this IS the delivery, on every trigger. A run whose summary is
# empty because an earlier step died is itself the signal.
if: always()
run: |
{
echo "### Required-set patrol — sweep exit ${{ steps.sweep.outputs.exit_code }}"
echo
echo '```'
cat "$RUNNER_TEMP/required-set.md" 2>/dev/null || echo '(no report produced)'
cat "$RUNNER_TEMP/required-set.err" 2>/dev/null || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Annotate a completed sweep that found drift
# A finding is NOT a failure (see the header) — but a green check that
# hides one is the #4690 shape with a tick on it, so a completed sweep
# that did not render its clean mark gets an annotation on the run list.
#
# The marker is the script's own `✅ the live required set and this
# registry agree in both directions.` line, and its ABSENCE is what is
# read: every non-agreeing reading the script can produce (direction A,
# direction B, a dry-run ruleset, no enforcing ruleset at all) withholds
# that line, and check-required-contexts' self-test pins both directions
# of that rendering — including this workflow's dependence on it by
# name. Erring this way is deliberate: a rendering change costs a FALSE
# ALARM, never a false all-clear.
if: steps.sweep.outputs.exit_code == '0'
run: |
if grep -qF '✅' "$RUNNER_TEMP/required-set.md"; then
echo "::notice::required-set sweep: the live required set and REQUIRED_CONTEXTS agree in both directions."
else
echo "::warning::required-set sweep found a registry-vs-live DISAGREEMENT. Nothing is blocked and nothing is judged — read this run's summary. Direction A (a registry row that is not live) is the maintainer's to resolve; direction B (a live context with no row) is a dev's."
fi
- name: Fail the run if the sweep could not run
# LAST, on purpose: the summary carries the classified output BEFORE the
# job goes red, so the run that raised the alarm also holds the reason.
#
# Findings are NOT a failure condition and never reach here: exit 0 with
# 40 disagreements is a successful patrol.
if: steps.sweep.outputs.exit_code != '0'
run: |
echo "::error::check-required-contexts --verify-required-set exited ${{ steps.sweep.outputs.exit_code }} — the standing patrol did not read the live required set. NOT VERIFIED is not a clean reading (#4690); see this run's summary."
exit 1
# ══ `platform-checklist-watchdog` — formerly `.github/workflows/platform-checklist-watchdog.yml` ══
#
# 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 job's triggers are this file's: `schedule:` (its own cron, read by
# its `if:`), `workflow_dispatch:`, and a `pull_request:` trigger
# `paths`-filtered to THIS FILE AND THE SIBLING JOBS' OWN INPUTS, nothing
# else — the list is frozen as a set in
# `scripts/check-platform-checklist-watchdog.mjs` — the posture
# `half-state-patrol.yml` and every job in this file 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 or a sibling patrol's own script
# blocks no unrelated pull request at all — this job publishes no required
# context and never reds on checklist drift — so 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.
platform-checklist-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
# One file, four clocks: a `schedule` event carries the cron that fired it, so
# each job runs only on its own cron; `workflow_dispatch` runs the one the input
# names (default: all four); a `pull_request` run exercises every job.
if: >-
(github.event_name != 'schedule' || github.event.schedule == '51 2 * * *')
&& (github.event_name != 'workflow_dispatch' || inputs.patrol == 'all' || inputs.patrol == 'platform-checklist-watchdog')
runs-on: ubuntu-latest
timeout-minutes: 15
# 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 * * *'
steps:
- name: Checkout repository
uses: actions/checkout@v7
# `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 workspace
uses: ./.github/actions/setup-workspace
with:
install: 'false'
# 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
# ══ `release-coverage-patrol` — formerly `.github/workflows/release-coverage-patrol.yml` ══
#
# The standing caller for `scripts/check-release-section-coverage.mjs` (#10803).
#
# ## Why a workflow, when the gate ALREADY runs on every PR
#
# Unlike `half-state-patrol.yml` and the `required-set-patrol` job, this check does
# not need a runner for TRANSPORT reasons. It is fully offline — it reads
# `packages/spec/CHANGELOG.md` and `content/docs/releases/**` straight out of the
# tree, opens no socket, and runs inside `lint.yml` on every pull request today.
# Measured, not assumed: a network-shaped scan of the script (`fetch(`, `http`,
# `api.github.com`, `execSync`, `child_process`, `octokit`, `GITHUB_TOKEN`)
# matches ZERO lines, against 4 matches each in those two patrols' workflows.
#
# So the reason for a standing caller here is AUDIENCE, not reachability, and it
# was measured on a live gap rather than argued (#10803):
#
# 17.2.0's CHANGELOG heading landed 2026-08-23T06:29:35Z. Over the next 40
# hours, 262 pull requests landed on `main`. Every one of their `lint.yml`
# runs emitted the two advisory `::warning::` annotations. Commits touching
# `content/docs/releases/` in that window: ZERO. Open PRs authoring the
# section: ZERO. #10268's acceptance criterion is "visible within a day of a
# minor publishing" — already exceeded, with the advisory in place and firing.
#
# Three structural reasons that reading is not bad luck, all verified against a
# real PR head sha through the check-runs API:
#
# 1. The annotation is registered at `path=.github`, line 19/20 — the WORKFLOW
# file, because the gate emits `::warning::` with no `file=` parameter. It
# therefore cannot appear on `content/docs/releases/v17.mdx` in the
# Files-changed tab, which is the only place a reviewer reads by default.
# 2. It rides a GREEN required check. That PR head carried 31 check runs, 0 of
# them failing, which GitHub collapses to "All checks have passed". It is
# step 74 of 106 in that job.
# 3. It is addressed to whoever opened the next PR. That author did not create
# the debt and does not own the remedy. A finding every PR author sees and
# no PR author owns is the shape that stays unfixed for 25 days.
#
# Volume is NOT the problem and this file does not pretend otherwise: those two
# annotations were the only `##[warning]` lines in a 4004-line job log, and 2 of
# only 5 annotations across all 31 checks. The advisory is quiet and correct. It
# is simply pointed at the wrong reader, and no amount of shouting on a PR fixes
# that. This workflow adds the reader; it does not replace the advisory.
#
# ## Why this calls `--strict`, and why it still cannot go red
#
# `--strict` promotes findings to exit 1 "for a caller that OWNS the remedy". It
# had no standing caller at all, which is the defect #10803 records. It has one
# here — as a PREDICATE, never as a verdict.
#
# The two runs below are not redundant, and the order is load-bearing. The gate
# deliberately returns 1 for BOTH "findings, under --strict" and "the instrument
# is broken", so `--strict` alone is ambiguous and could not be branched on:
#
# run 1, ADVISORY — exit 0 means the instrument READ the corpus (findings or
# not); non-zero means it could not, and that is fatal in
# every mode by the gate's own design. This run also
# produces the authored, human-readable finding text.
# run 2, --STRICT — with the instrument already proven healthy by run 1,
# exit 1 now means exactly one thing: findings exist.
#
# That is what lets the patrol branch on a machine-readable boolean instead of
# grepping prose, while the JOB stays green whenever findings exist. A `--strict`
# run stays red for as long as the prose is unwritten — 25 days, in the 16.1.0
# case — and a chronically red scheduled job is how people learn to skim red,
# which the retired `prerelease-pin-watch.yml`'s header warned about directly. Findings land
# in the anchor issue; they never colour this job.
#
# ## What lands where
#
# One pinned ANCHOR ISSUE, rewritten in place every run (`ANCHOR_ISSUE` below) —
# `half-state-patrol.yml`'s shape and its argument, which applies verbatim here:
# never a comment per run, because a per-run comment stream is a second tracker
# nobody prunes and GitHub's edit history is already the archive this needs.
#
# It fits this finding particularly well. The population is tiny — 5 in-scope GA
# minors exist in total (16.0.0, 16.1.0, 17.0.0, 17.1.0, 17.2.0) — so the anchor
# is quiet almost always, and the remedy is owner-shaped: one person writes one
# curated section. An issue can be assigned. An annotation cannot.
#
# The `Swept` timestamp is the patrol's heartbeat and is refreshed even when the
# findings are unchanged: a timestamp that stops advancing is how a reader learns
# the standing caller died. The run must not "optimize away" that no-op edit.
#
# ## Report-only, and the one thing that is NOT report-only
#
# Findings never fail anything. This job never writes release prose — it cannot,
# and the gate's header argues why at length: a curated section is a judgement
# about what is user-facing, read out of 69 package changelogs, and this gate
# GENERATES NOTHING.
#
# The job DOES fail when the sweep could not run, or when its report could not be
# delivered. That is not a gate on the release pages; it is the patrol reporting
# its own death. Silent non-delivery would leave a stale anchor body that reads
# exactly like a clean corpus — "could not read the input" must never look like
# "input is clean" (#4690), with a heartbeat on it. Failing costs nobody a PR:
# this workflow gates no branch and blocks no queue.
release-coverage-patrol:
name: Release section-coverage sweep
# One file, four clocks: a `schedule` event carries the cron that fired it, so
# each job runs only on its own cron; `workflow_dispatch` runs the one the input
# names (default: all four); a `pull_request` run exercises every job.
if: >-
(github.event_name != 'schedule' || github.event.schedule == '19 4 * * *')
&& (github.event_name != 'workflow_dispatch' || inputs.patrol == 'all' || inputs.patrol == 'release-coverage-patrol')
runs-on: ubuntu-latest
timeout-minutes: 10
# Least privilege: this job reads the repo and writes exactly one issue BODY.
# `issues: write` is the narrowest scope GitHub offers for that edit; the job
# never uses it for labels, comments, assignees or state, and the gate it calls
# is read-only against the filesystem by construction.
permissions:
contents: read
issues: write
# One patrol at a time. A scheduled run overlapping a manual dispatch would have
# two runs racing to rewrite the same body, and the loser's findings would vanish
# with no trace but an edit-history entry.
concurrency:
group: release-coverage-patrol
cancel-in-progress: false
env:
# The pinned anchor issue whose body this workflow owns.
#
# Resolution: the repository variable `RELEASE_COVERAGE_ANCHOR_ISSUE` if set,
# else this repo's own pinned number, else EMPTY — and empty makes the job
# refuse to write rather than guess. The literal is guarded by the repository
# name on purpose: an unguarded fallback would let a verbatim copy in a sibling
# repo rewrite ITS #11879 — some unrelated card — with this repo's findings,
# silently, every night. A number is only meaningful in the repo it was minted
# in.
#
# TO ROTATE (here): open a new `tracking`-labeled issue, put its number below,
# and note the handover in the OLD issue's body before closing it (its edit
# history is the archive and does not travel).
# TO ADOPT (a sibling repo): change NOTHING here — set the repository variable.
#
# ⚠️ Folded scalar, and every continuation line sits at the SAME indent on
# purpose: a more-indented line in a `>-` block keeps its newline literally,
# which would hand the expression parser a multi-line string instead of one
# expression.
ANCHOR_ISSUE: >-
${{ vars.RELEASE_COVERAGE_ANCHOR_ISSUE
|| (github.repository == 'objectstack-ai/objectstack' && '11879')
|| '' }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
# No `pnpm install`: the gate imports `node:fs` and one local module.
# Installing the workspace here would buy nothing and would give a
# scheduled patrol a lockfile it could fail on.
#
# No `--self-test` either. lint.yml runs it on every pull request, so this
# job would be re-proving a property already proven on the commit it just
# checked out — and two node invocations in one `run:` block is precisely
# the shape `check:step-collector` exists to refuse, because the first
# one's failure is masked by the second.
- name: Sweep the corpus
id: sweep
run: |
# ── run 1: ADVISORY. Exit code answers "did the instrument READ the
# corpus", which is the only thing that may colour this job.
set +e
node scripts/check-release-section-coverage.mjs \
> "$RUNNER_TEMP/report.txt" 2> "$RUNNER_TEMP/report.err"
advisory_code=$?
set -e
# Captured with NO pipe in between. `cmd | tail` would report the
# PIPE's status — `tail` essentially never fails, so a healthy and a
# broken instrument would both read as 0.
echo "advisory_code=$advisory_code" >> "$GITHUB_OUTPUT"
echo "advisory run exited $advisory_code"
# ── run 2: --STRICT, used ONLY as a findings predicate. Meaningful
# solely because run 1 has already established the instrument is
# healthy; the gate returns 1 for a broken instrument too.
strict_code=0
if [ "$advisory_code" -eq 0 ]; then
set +e
node scripts/check-release-section-coverage.mjs --strict \
> "$RUNNER_TEMP/strict.txt" 2> "$RUNNER_TEMP/strict.err"
strict_code=$?
set -e
echo "strict run exited $strict_code"
else
echo "instrument is broken — skipping the --strict predicate"
fi
echo "strict_code=$strict_code" >> "$GITHUB_OUTPUT"
cat "$RUNNER_TEMP/report.err" >&2 || true
- name: Resolve the anchor issue
# An install with no anchor configured has nowhere to land its report,
# and the ONLY safe behaviour is to say so loudly. The two alternatives
# are both the failure this file exists to prevent: guessing a number
# would rewrite an unrelated card in this repo, and skipping the write
# quietly would leave a patrol that runs, finds, and tells nobody —
# indistinguishable from a clean corpus.
#
# Placed AFTER the sweep so the run summary still carries the findings,
# and skipped on a pull_request run, which never writes an anchor.
if: github.event_name != 'pull_request'
run: |
if [ -z "${ANCHOR_ISSUE//[[:space:]]/}" ]; then
echo "::error::No anchor issue configured for ${{ github.repository }}. The sweep RAN (see the run summary) but has nowhere to land. Open a \`tracking\`-labeled anchor issue in this repo and set the repository variable RELEASE_COVERAGE_ANCHOR_ISSUE to its number (Settings -> Secrets and variables -> Actions -> Variables)."
exit 1
fi
case "$ANCHOR_ISSUE" in
*[!0-9]*|'') echo "::error::RELEASE_COVERAGE_ANCHOR_ISSUE is '$ANCHOR_ISSUE', which is not an issue number."; exit 1 ;;
esac
echo "anchor: #$ANCHOR_ISSUE in ${{ github.repository }}"
- name: Render the report
id: render
env:
ADVISORY_CODE: ${{ steps.sweep.outputs.advisory_code }}
STRICT_CODE: ${{ steps.sweep.outputs.strict_code }}
# This workflow's own `cron:`, handed to the renderer so the anchor
# body states the cadence that makes its heartbeat line decidable
# (#17720). It lives HERE rather than in the script because the
# schedule is declared here: a second copy in the renderer would be a
# second place to drift from, and a drifted deadline reads exactly
# like a correct one. ⛔ Unset, the renderer says so in the body
# rather than assuming a period — it never guesses.
PATROL_SCHEDULE: '19 4 * * *'
PROVENANCE: >-
run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
· commit `${{ github.sha }}` · trigger `${{ github.event_name }}`
# Runs its own `--self-test` first (#11150). That leg is not ceremony
# here: the renderer's assertions pin `verdict()` — the mapping from the
# sweep's two exit codes onto did-not-run / findings / clean — and a
# broken mapping still renders a body. The live run below would stay
# exit 0 and publish a WRONG report, including the one reading #4690
# forbids, a broken instrument rendered as a clean corpus. Only the live
# run is redirected; the self-test's own verdict goes to the step log.
run: |
node scripts/render-release-coverage-anchor.mjs --self-test
node scripts/render-release-coverage-anchor.mjs > "$RUNNER_TEMP/body.md"
# The rendered body is the product of the run, so it goes to the step
# summary on EVERY trigger — including the pull_request runs that must not
# touch the anchor. A reader of this workflow's own PR sees exactly what