Repository navigation
525 lines (516 loc) · 30.3 KB
/
Copy pathhalf-state-patrol.yml
File metadata and controls
525 lines (516 loc) · 30.3 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
name: Half-State Patrol
# The standing caller for `scripts/pm/check-half-states.mjs` (#9844).
#
# Since #18471 this file is a CALLER and nothing else: the patrol's body lives
# in `.github/actions/half-state-patrol`, a composite action, and that action's
# header is the authority on how the patrol works. What stays here is what is
# NOT repo-agnostic — when it runs, what it may touch, the board's own anchor,
# and the one step that writes to this repo's cards.
#
# ## Why a workflow at all — the transport, not the calendar
#
# ⚠️ HALF SUPERSEDED (ruling #208 on objectstack#19491, executed by
# objectstack#19497): this file declares NO schedule. The argument below is kept
# as the history that put the patrol on a runner, not as a promise that anything
# fires it. What survives is the transport reason in the next paragraph; what the
# ruling retired is the calendar.
#
# The sweeper carries thirteen predicates over the dispatch protocol's
# label/assignee/PR invariants, and for most of its life its documented consumer
# was "a PM seat's patrol round" — which is to say, nobody's calendar. A shift
# covering two lanes declared a queue empty from memory while eight malformed
# claims (H2) and an unenumerated backlog sat on the board. Not one predicate had
# fired. A healing mechanism with no scheduled caller heals only in the
# counterfactual, and an alarm added to a script nobody runs is still silence.
# That reading bought the schedule in #9844 and was true of its day. Ruling #208
# re-measured the same instrument and found the standing caller charging more
# than it healed — see the `on:` block below, which carries the reading and the
# ⛔ that goes with it.
#
# "Some seat should run it" also kept not happening for a MEASURED reason, not a
# discipline one: in every container class measured at the time, the live sweep
# could not run at all (#7412 class 1 — api.github.com refuses that egress in
# both directions, with and without a token). The fix therefore had to move the
# caller somewhere the transport prerequisite is actually met. A GitHub Actions
# runner with the workflow's own `GITHUB_TOKEN` is that place — #7412 class 2,
# the triage Routine container, is the same shape and measured reachable with
# 15,000 core quota.
#
# ⚠️ CORRECTED (#13544, measured 2026-08-31): "cannot run inside an agent
# container at all" is no longer true of every such container, and the claim
# above is kept only as the history that put this workflow here. A proxied agent
# container reaches api.github.com fully — `/rate_limit`, `/user` AND
# `GET /repos/{owner}/{repo}` all 200 with `server: github.com` — once node's
# fetch is routed through the session proxy, which the script now does for
# itself. What had actually failed was the ROUTE: node's `fetch` ignores
# `HTTPS_PROXY`, so the sweeper sent the proxy's placeholder token straight to
# GitHub, earned a 401, and reported that refusal as the container's verdict.
#
# ⛔ That did NOT retire this workflow either, and the fix deliberately did not
# touch it: a correction about WHO CAN run the sweep is not a ruling about
# WHETHER it should run by the clock, and this file stayed exactly as it was.
# What the correction bought is the path this workflow now runs on ALONE — a lane
# that needs the board read right now (the body trims its own rows, and says so)
# gets it from its own container or from a `workflow_dispatch` here. Since ruling
# #208 that on-demand read is the only read there is.
#
# ## What lands where
#
# One pinned ANCHOR ISSUE, rewritten in place every run (`anchor-issue` below).
# Never a comment per run: the board is one board, a per-run comment stream would
# be a second tracker that nobody prunes, and GitHub's edit history is already the
# archive this needs. The body is owned end-to-end by the generator, so no run can
# leave half of it stale.
#
# The `Swept` timestamp in that body is refreshed even when the findings are
# unchanged, and the run must still not "optimize away" that no-op edit — but
# read it for what it now is. With no schedule behind it, the stamp dates the
# last time a seat ASKED for a reconciliation; it is no longer a heartbeat, and a
# `Swept` that stopped advancing means nobody has dispatched the patrol since,
# ⛔ not that a standing caller died.
#
# ⚠️ KNOWN STALE READING, deliberately not repaired here (objectstack#19497): the
# generator stamps a cadence deadline beside `Swept` from its own `PATROL_CRON` /
# `PATROL_CADENCE_HOURS` constants, so the anchor body will keep stamping an
# "expected every 6h · next by …" deadline computed from the schedule string
# those constants still hold — a deadline against a schedule this file no longer
# declares, and nothing on this board fires. The ruling forbids editing the
# sweeper for it (⛔ not one of its 36,717 lines); the repair is filed as its own
# finding instead.
#
# ## Report-only, and the one thing that is NOT report-only
#
# Findings never fail anything. A completed sweep exits 0 whether it found 0 or 40
# half-states, this job never writes a label, never closes a card, never fixes a
# state, and no H-predicate is a blocking gate — the script's own header argues
# that at length (a half-state is a fact about a live shared board, not about
# whichever PR happens to run CI next).
#
# 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 board; it is the patrol reporting its own
# death. A workflow that quietly does nothing because a credential lapsed is the
# exact shape this repo keeps having to fix (#4449, #9575), and it is doubly
# unacceptable here: silent non-delivery would leave a stale anchor body that
# reads exactly like a clean board — the #4690 failure ("could not read the input"
# must never look like "input is clean") with a timestamp on it. Failing costs
# nobody a PR: this workflow gates no branch and blocks no queue.
#
# ## Adopting the patrol in a sibling repo (#11217, #18471)
#
# ⛔ Copy NOTHING. A sibling board installs the patrol by CALLING the action,
# pinned to a sha of this repository, and passing its own constants:
#
# jobs:
# patrol:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v7
# - uses: actions/setup-node@v7
# with:
# node-version: '22'
# - uses: objectstack-ai/objectstack/.github/actions/half-state-patrol@<sha>
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# anchor-issue: ${{ vars.HALF_STATE_ANCHOR_ISSUE }}
#
# plus one `tracking`-labeled anchor issue opened in that repo, whose number is
# what `anchor-issue` carries. ⛔ A sha, never `@main`: `@main` is the drift this
# change removes, re-entered from the other side. A board with no anchor BY
# DECISION adds `anchor-optional: true` instead of opening one, and its findings
# then reach run summaries only; the action's header says why that is an opt-in
# (#20793), and this caller keeps the default.
#
# ⚠️ The paragraph this replaces was a LIST of files to copy, and it is worth
# recording why a list is not the fix. It said TWO files until 2026-09-03 while
# the sweeper had imported a third since well before that — so the documented
# install was a patrol that could not start; a clean two-file copy died with
# `ERR_MODULE_NOT_FOUND … /scripts/invoked-as.mjs`, exit 1, before a single
# predicate ran. It was corrected, and two weeks later it was short by four
# again. A hand-kept mirror of an import graph goes stale the way a hand-kept
# copy of a script goes stale, which is to say: by arithmetic, not by
# carelessness. The list is deleted rather than lengthened — the runner now
# places this whole repository at the pinned sha for the action, so the import
# graph travels with the code and the action asserts its own sources are there.
#
# ⛔ Each install still uses its OWN `secrets.GITHUB_TOKEN` and reads its own
# repo. No cross-repo credential, no matrix over repos, no PAT: that route was
# refused at grading (it buys no coverage a per-repo install lacks and raises the
# credential floor for every repo at once). Calling a PUBLIC repo's action from a
# private one needs no credential at all, so nothing here reopens it. The
# accepted consequence is unchanged: a cross-repo `Blocked-by:` target stays
# UNJUDGED in each install — H19 says so in its own row rather than reading it as
# a healthy block.
on:
# ⛔ ON DEMAND ONLY — this workflow declares NO schedule, and re-adding one
# takes the maintainer's sentence, not a reading of the history above.
# Ruling #208 on objectstack#19491, executed by objectstack#19497, maintainer
# 「19491 接受你的建议,并立刻派发处理相关任务。」 The triage seat dispatches this
# workflow when it wants the board reconciled; nothing fires it by the clock.
#
# WHAT THE RETIRED SCHEDULE WAS: `37 1,7,13,19 * * *` — four runs a day, six
# hours apart, the minute deliberately off the top of the hour so the sweep
# never read the board mid-heal while the hourly triage Routine was pairing
# states, and four runs rather than hourly because H13's own threshold is 2h.
# None of that was what the ruling found wrong.
#
# WHY IT WENT: the patrol is REPORT-ONLY (see the section above) while the
# dispatch charter made its anchor rows a precondition for opening a dispatch.
# A row misreading a now-legal state therefore cost every executive seat a
# handling step — four times a day, until a dev repaired the row — and the
# sweeper was taking eight such repairs a day in the fortnight before the
# ruling. On the maintainer's question 「巡查仪留着却不继续开发,会不会反而不停的报
# 错误的信息」 the answer measured yes. The ruling stops the clock, ⛔ not the
# instrument: the script keeps every one of its 36,717 lines and stays runnable
# from here, and the charter's precondition is deleted on the sibling card.
#
# ⛔ Do not re-add a `schedule:` block on the argument the history above makes
# ("an alarm nobody runs is silence"). That argument is answered, not
# forgotten: it was weighed against what the standing caller was charging every
# seat, and it lost. If a month passes with nobody dispatching this, the ruling
# is that the sweeper is deleted whole on its own card — ⛔ never quietly
# re-scheduled.
#
# The cadence was always per-board and stays in the caller: a sibling repo that
# wants a scheduled sweep of ITS board declares `schedule:` in its own copy of
# this caller and picks its own minute against its own healer — never in the
# action, and never by reading a cadence out of this file.
workflow_dispatch: {}
# Changes to the patrol itself get exercised before they merge — the same
# posture as engine-split-metric.yml. On a pull_request run the sweep still
# executes (that is the point: the transport, the flags and the rendering are
# proven on a real runner), but the anchor write is skipped and the rendered
# body goes to the run's step summary instead. A PR must never rewrite the
# board's pinned view.
#
# This repo is the patrol's FIRST CONSUMER and calls the action by its local
# path, so a PR editing the action runs against the edit rather than against
# a pinned copy of what is already on `main`. That is the whole value of being
# the first consumer, and it is why the action's own directory is a trigger
# path below.
pull_request:
paths:
- 'scripts/pm/check-half-states.mjs'
# The sweeper imports this helper, so a change to it can break the patrol
# without touching either file beside it — and the PR-time proof this
# trigger exists to give would not run. An undeclared dependency is
# undeclared in every place that has to name it.
- 'scripts/invoked-as.mjs'
# The closed-card sweep this workflow also calls (#16005). Same reasoning
# as the row above, one file along: a step whose script can change without
# this trigger firing is a step whose PR-time proof is a coincidence.
- 'scripts/pm/sweep-closed-cards.mjs'
# The citation census this workflow also calls (#18224). Same reasoning as
# the two rows above, one file along: a step whose script can change
# without this trigger firing is a step whose PR-time proof is a
# coincidence.
- 'scripts/check-issue-citations.mjs'
# The patrol's body. A glob, not the one file: anything added to that
# directory is part of what runs here.
- '.github/actions/half-state-patrol/**'
- '.github/workflows/half-state-patrol.yml'
# Least privilege: this job reads the repo and writes exactly one issue BODY,
# plus the labels and comment the closed-card sweep is ruled to write. `issues:
# write` is the narrowest scope GitHub offers for either edit; the sweeper the
# patrol calls is read-only against the API by construction.
#
# `pull-requests: read` is READ-ONLY and buys one thing, for the citation
# census step only — MEASURED on run 35495222460, this workflow's own
# pull_request run WITHOUT this row: the census reported 3,628 unresolvable
# citation sites where a full-scope read of the same tree reported 2,168, and
# the difference is 1,460 — EXACTLY the `resolves-as-pull-request` tally.
# `GET /repos/{owner}/{repo}/issues` answers with the pull requests omitted
# unless this scope is held, so every citation naming a PR number was reported
# as a number the board never had. A report-only reading that is wrong by 67%
# is still a machine-readable surface telling a lie. ⛔ Do not drop this row
# as tidying, and ⛔ do not widen it to `write`: nothing here writes a PR.
permissions:
contents: read
issues: write
pull-requests: read
# One patrol at a time. Two dispatches in flight at once — the shape that is left
# now that nothing fires by the clock — 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: half-state-patrol
cancel-in-progress: false
jobs:
patrol:
name: Live half-state sweep
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
# The patrol reads the SWEPT repo's own checkout for the passes that
# classify its workflows and its tracked files, so the checkout is the
# subject of the sweep rather than a source of the code that runs it.
- name: Checkout repository
uses: actions/checkout@v7
# Stays in the caller on purpose, and ⛔ NOT for a census reason: the Node
# census reads `.github/actions/**` too. The patrol action holds no Node
# pin of its own and only asserts a floor from the `.nvmrc` it shipped
# with, so the runner's Node has to be the CALLER's choice for that check
# to be a reading about anything. The action's header carries the measure.
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
# No `pnpm install`: the sweeper imports only `node:` builtins
# (`process`, `child_process`, `fs`, `url`), global `fetch`, and one
# repo-local helper — no npm dependency, so installing the workspace here
# would buy nothing and would give this patrol a lockfile it could fail
# on.
#
# `./` — the LOCAL action, deliberately, and this is the one consumer that
# should not pin a sha. A sibling pins one because it wants a reviewed
# moment between upstream landing a change and its board adopting it; this
# repo wants the opposite, because a PR editing the action must be proven
# by this job before it merges. Pinning here would exercise `main`'s copy
# of the thing under review.
- name: Sweep the board and update the anchor
uses: ./.github/actions/half-state-patrol
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# The pinned anchor issue whose body the patrol owns — this board's
# own number, and the one constant this file exists to carry.
#
# Resolution: the repository variable `HALF_STATE_ANCHOR_ISSUE` if
# set, else this repo's own pinned number, else EMPTY — and empty
# makes the action refuse to write rather than guess, because this
# caller leaves `anchor-optional` at its loud default. The literal is
# guarded by the repository name even though this file no longer
# travels: the cheapest way to adopt the patrol is still to copy this
# caller, and an unguarded fallback is what would let such a copy
# rewrite some unrelated card in a sibling with this board's findings,
# silently, on every run it is given. A number is only ever meaningful
# 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 — pass your own
# number to the action from your own caller.
#
# The anchor deliberately carries `tracking` and NO `domain:*` label:
# `tracking` is in the sweeper's own H13_EXEMPT_LABELS, so the anchor
# can never appear as a finding in the sweep it hosts.
#
# ⚠️ Folded scalar, and every continuation line sits at the SAME
# indent on purpose: a more-indented line in a `>-` block keeps its
# newline literally (measured on this very value), which would hand
# the expression parser a multi-line string instead of one expression.
anchor-issue: >-
${{ vars.HALF_STATE_ANCHOR_ISSUE
|| (github.repository == 'objectstack-ai/objectstack' && '9857')
|| '' }}
- name: Sweep the closed cards
# #16005 — the pm-loop state labels are CLAIMS that work is in flight,
# and GitHub leaves every label in place when a merged `Fixes` pull
# request closes a card. The seat was paying a hand round trip per
# landing to remove them (eighteen identical ones in one measured
# shift). This step is that stroke, mechanized; the script's header
# carries the ruling it obeys and the window that keeps it to
# close-time hygiene rather than the backfill the 2026-08-31 maintainer
# ruling refused.
#
# ⛔ NOT in the composite action. It is the one step of the old file
# that was never repo-agnostic: it WRITES to cards under a ruling this
# repo's board took, and no sibling has taken it.
#
# ⛔ This step never fails the job, whatever the sweep returns; the
# alarm rides an annotation and the run summary instead. Findings are
# not a failure condition here either — the same posture the patrol
# takes.
#
# It runs AFTER the patrol rather than before it, which is the one
# ordering change #18471 made. The anchor's content is unaffected: the
# half-state sweep still reads the board before this step touches it.
# What changes is that the patrol's product — the anchor write — can no
# longer be starved by anything this step does, which is what the old
# placement's own comment asked for and did not get. `!cancelled()`
# rather than an implicit `success()`: a patrol run that went red
# because it could not reach the board says nothing about whether this
# repo's closed cards still carry stale labels.
if: ${{ !cancelled() && github.repository == 'objectstack-ai/objectstack' }}
id: closed-cards
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The board this run acts on is the repo this workflow lives in.
PM_SWEEP_REPO: ${{ github.repository }}
PROVENANCE: >-
posted by half-state-patrol [run ${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
· trigger `${{ github.event_name }}`
# A pull_request run PROVES the step — the transport, the flags and
# the rendering on a real runner — and writes nothing, exactly as the
# anchor write is skipped for it. That convention is this patrol's and
# it is load-bearing: a PR must never write to the board.
SWEEP_MODE: ${{ github.event_name == 'pull_request' && '--dry-run' || '--write' }}
run: |
# The judge's own cases first, and the sweep only if they hold: this
# step WRITES to other people's cards, and a broken predicate that
# still runs is the one failure mode that cannot be undone by the next
# run. `check:pm-closed-card-sweep` is the same command under a dev
# -facing name; this is the invocation CI holds.
set +e
node scripts/pm/sweep-closed-cards.mjs --self-test > "$RUNNER_TEMP/closed-cards-selftest.log" 2>&1
selftest=$?
set -e
cat "$RUNNER_TEMP/closed-cards-selftest.log"
if [ "$selftest" != "0" ]; then
echo "exit_code=$selftest" >> "$GITHUB_OUTPUT"
{
echo "### Closed-card sweep — SKIPPED: its own self-test failed (exit $selftest)"
echo
echo '```'
cat "$RUNNER_TEMP/closed-cards-selftest.log"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::closed-card sweep self-test failed (exit $selftest) — the sweep did NOT run and wrote nothing. Nothing here is a reading about the board."
exit 0
fi
set +e
node scripts/pm/sweep-closed-cards.mjs "$SWEEP_MODE" --provenance="$PROVENANCE" \
> "$RUNNER_TEMP/closed-cards.md" 2> "$RUNNER_TEMP/closed-cards.err"
code=$?
set -e
# Captured with NO pipe in between: piped, `$?` is the pipe's status
# and a red run and a green one read the same.
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
{
echo "### Closed-card sweep — exit $code (\`$SWEEP_MODE\`)"
echo
echo '```'
cat "$RUNNER_TEMP/closed-cards.md" 2>/dev/null || echo '(no report produced)'
echo '```'
echo
echo '<details><summary>stderr</summary>'
echo
echo '```'
cat "$RUNNER_TEMP/closed-cards.err" 2>/dev/null || true
echo '```'
echo
echo '</details>'
} >> "$GITHUB_STEP_SUMMARY"
cat "$RUNNER_TEMP/closed-cards.err" >&2 || true
if [ "$code" = "3" ]; then
echo "::error::closed-card sweep exited 3 — it could NOT read the board, so it says nothing about whether residue is accumulating. See this run's summary."
elif [ "$code" != "0" ]; then
echo "::warning::closed-card sweep exited $code — at least one card was left UNJUDGED. An unjudged card is not a clean card; see this run's summary."
fi
- name: Census the repo's issue citations
# #17512's gate, wired here by #18224 — the REPORT-ONLY half, and the
# posture is a ruling, not a preference. `--census` judges every
# citation in the gate's declared surfaces (the published release pages
# and package source docblocks), which is ~2,785 unresolvable sites on a
# tree nobody touched, and its verdict is NOT a function of this tree:
# #16783, #16786 and #16787 were measured RESOLVING on 2026-09-10 and
# 404 on 2026-09-14 with no change to this repository. That is exactly
# the shape this workflow exists for — a fact about a live shared board,
# not about whichever change happens to run CI next — so it belongs on
# the patrol lane and ⛔ NEVER on a blocking one. The DIFF-scoped half of
# the same gate is the blocking one and lives in `lint.yml`; the two
# postures are opposite on purpose and ⛔ neither moves to the other's
# lane.
#
# ⛔ NOT in the composite action this job `uses:` above — it is a step of
# the CALLER, for the same reason the closed-card sweep one step up
# states, plus two that are specific to a census of a TREE. All three
# are measurable on this repo rather than argued:
#
# - It is not repo-agnostic. `scripts/check-issue-citations.mjs` is
# objectstack-only, so the action's own "Locate the patrol sources"
# step would have to either name it — and then REFUSE to run in
# every sibling that adopted the action, which is the one thing that
# action exists to make possible — or not name it, and leave a step
# inside the action failing on a missing file in that sibling. The
# repo-name gate below is what answers both, and it belongs where
# the repository is the caller's own.
# - The action runs its scripts from `steps.sources.outputs.root`, the
# tree the ACTION ships from, while the board's checkout is
# `github.workspace`; that split is deliberate and the action's own
# comments carry it. This census's subject is the WORKSPACE tree's
# declared surfaces, so from inside the action a sibling's run would
# census objectstack's own release pages and report the count under
# the sibling's name — a machine-readable surface telling a lie, the
# failure this step's `permissions:` note above is about. In the
# caller the two directories are the same one, on every install.
# - `pull-requests: read` is granted by the `permissions:` block of
# this file. A composite action declares no permissions and the
# action's input documentation names only `issues: write` and
# `contents: read`, so a census inside it would silently depend on a
# scope no adopting caller was told to grant — measured above as a
# reading wrong by 67%. A scope and its one consumer stay in one
# file.
#
# WHERE THE REPORT GOES: this run's step summary and job log, plus one
# `::warning::` carrying the site count. ⛔ NOT the anchor issue — that
# body is owned end-to-end by `check-half-states.mjs`'s generator, and a
# second writer is how half of a generated body goes stale.
# HOW OFTEN: once per `workflow_dispatch` — this workflow has carried no
# schedule since ruling #208 (objectstack#19497) — plus the
# `pull_request` runs the paths filter above admits.
# WHAT IT COSTS AND WHO PAYS: the census enumerates the whole board once
# (159 requests on this repo at the time of writing, cursor-paginated —
# the alternative is one request per distinct number). It is paid by
# THIS repository's own `secrets.GITHUB_TOKEN` core quota, the same
# 5,000/hour this job already draws the live sweep from: ~159 requests
# per run, ~3% of a single hour's allowance, and no standing daily draw
# at all now that a seat has to ask for each run. ⛔ No PAT, no
# cross-repo credential — the file's own rule.
#
# ⛔ Gated on the repository NAME, for the reason the closed-card sweep
# above states: this script is objectstack-only until a sibling has a
# copy, and a verbatim copy of this workflow elsewhere must SKIP rather
# than fail on a missing file.
#
# ⛔ `!cancelled()` rather than an implicit `success()`, and this
# placement has to SPELL that rather than inherit it. While the patrol's
# steps were written inline in this file, this step sat ABOVE the one
# step that fails the job, so it ran whatever the sweep returned. The
# patrol is now a single `uses:` step that goes red itself when the
# sweep could not read the board, so a default `success()` here would
# skip the census on exactly the runs where the patrol is down. This
# step reads neither the sweep's exit code nor its files: a patrol that
# could not reach the board says nothing about whether the citations in
# this tree resolve.
#
# ⛔ This step never fails the job, whatever the census returns —
# findings are not a failure condition here, and neither is a census
# that could not read the board: that is an alarm (`::error::`), not a
# gate. LAST on purpose, after the patrol's own anchor write and after
# the closed-card sweep: the anchor is this patrol's product, this job
# has a 15-minute timeout, and a report-only reading must never be able
# to starve the thing the workflow exists to deliver.
if: ${{ !cancelled() && github.repository == 'objectstack-ai/objectstack' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set +e
node scripts/check-issue-citations.mjs --census \
> "$RUNNER_TEMP/issue-citations.md" 2> "$RUNNER_TEMP/issue-citations.err"
code=$?
set -e
# Captured with NO pipe in between, for the reason the two steps above
# state at length: piped, `$?` is the pipe's status and a red run and a
# green one read the same.
{
echo "### Issue-citation census — exit $code (report-only)"
echo
echo '```'
cat "$RUNNER_TEMP/issue-citations.md" 2>/dev/null || echo '(no report produced)'
echo '```'
echo
echo '<details><summary>stderr</summary>'
echo
echo '```'
cat "$RUNNER_TEMP/issue-citations.err" 2>/dev/null || true
echo '```'
echo
echo '</details>'
} >> "$GITHUB_STEP_SUMMARY"
cat "$RUNNER_TEMP/issue-citations.err" >&2 || true
if [ "$code" != "0" ]; then
echo "::error::issue-citation census exited $code — the board was NOT read, so this run says nothing about whether unresolvable citations are accumulating. A census that could not run is not a clean census. See this run's summary."
else
sites=$(sed -n 's/^.*census: \([0-9][0-9]*\) unresolvable citation site(s).*$/\1/p' "$RUNNER_TEMP/issue-citations.md" | tail -1)
echo "::warning::issue-citation census: ${sites:-unknown} unresolvable citation site(s) in the declared surfaces. Report-only — the blocking half judges only what a change ADDS."
fi