Skip to content

Commit 2497d75

Browse files
committed
feat(tooling): give check-release-section-coverage a nightly standing caller
The gate's advisory half runs on every PR inside lint.yml; its `--strict` half had no caller at all, so the only thing that ever executed was one green job's log line among 106 steps. Measured before building: the 17.2.0 gap has been live for 40 hours, 262 PRs landed on main in that window each emitting the two advisory annotations, and `content/docs/releases/` took zero commits. #10268's acceptance criterion is "visible within a day of a minor publishing", already exceeded with the advisory in place. Verified against a real PR head sha through the check-runs API, the annotation is registered at `path=.github` (no `file=` parameter), rides a green required check among 31 non-failing check runs, and is addressed to whoever opened the next PR — who neither created the debt nor owns the remedy. Volume is not the problem: those were the only two `##[warning]` lines in a 4004-line job log. Audience is. So this adds the reader, and leaves the advisory exactly as it is. The patrol runs the gate TWICE and the order is load-bearing: the gate returns 1 for both "findings under --strict" and "broken instrument", so advisory-first establishes instrument health and only then is `--strict`'s exit 1 unambiguous. That gives `--strict` a real standing caller as a machine-readable predicate while the job itself stays green — findings land in a pinned anchor issue, never in a chronically red scheduled job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
1 parent 4019e16 commit 2497d75

2 files changed

Lines changed: 538 additions & 0 deletions

File tree

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
name: Release-Coverage Patrol
2+
3+
# The standing caller for `scripts/check-release-section-coverage.mjs` (#10803).
4+
#
5+
# ## Why a workflow, when the gate ALREADY runs on every PR
6+
#
7+
# Unlike `half-state-patrol.yml` and `required-set-patrol.yml`, this check does
8+
# not need a runner for TRANSPORT reasons. It is fully offline — it reads
9+
# `packages/spec/CHANGELOG.md` and `content/docs/releases/**` straight out of the
10+
# tree, opens no socket, and runs inside `lint.yml` on every pull request today.
11+
# Measured, not assumed: a network-shaped scan of the script (`fetch(`, `http`,
12+
# `api.github.com`, `execSync`, `child_process`, `octokit`, `GITHUB_TOKEN`)
13+
# matches ZERO lines, against 4 matches each in those two patrols' workflows.
14+
#
15+
# So the reason for a standing caller here is AUDIENCE, not reachability, and it
16+
# was measured on a live gap rather than argued (#10803):
17+
#
18+
# 17.2.0's CHANGELOG heading landed 2026-08-23T06:29:35Z. Over the next 40
19+
# hours, 262 pull requests landed on `main`. Every one of their `lint.yml`
20+
# runs emitted the two advisory `::warning::` annotations. Commits touching
21+
# `content/docs/releases/` in that window: ZERO. Open PRs authoring the
22+
# section: ZERO. #10268's acceptance criterion is "visible within a day of a
23+
# minor publishing" — already exceeded, with the advisory in place and firing.
24+
#
25+
# Three structural reasons that reading is not bad luck, all verified against a
26+
# real PR head sha through the check-runs API:
27+
#
28+
# 1. The annotation is registered at `path=.github`, line 19/20 — the WORKFLOW
29+
# file, because the gate emits `::warning::` with no `file=` parameter. It
30+
# therefore cannot appear on `content/docs/releases/v17.mdx` in the
31+
# Files-changed tab, which is the only place a reviewer reads by default.
32+
# 2. It rides a GREEN required check. That PR head carried 31 check runs, 0 of
33+
# them failing, which GitHub collapses to "All checks have passed". It is
34+
# step 74 of 106 in that job.
35+
# 3. It is addressed to whoever opened the next PR. That author did not create
36+
# the debt and does not own the remedy. A finding every PR author sees and
37+
# no PR author owns is the shape that stays unfixed for 25 days.
38+
#
39+
# Volume is NOT the problem and this file does not pretend otherwise: those two
40+
# annotations were the only `##[warning]` lines in a 4004-line job log, and 2 of
41+
# only 5 annotations across all 31 checks. The advisory is quiet and correct. It
42+
# is simply pointed at the wrong reader, and no amount of shouting on a PR fixes
43+
# that. This workflow adds the reader; it does not replace the advisory.
44+
#
45+
# ## Why this calls `--strict`, and why it still cannot go red
46+
#
47+
# `--strict` promotes findings to exit 1 "for a caller that OWNS the remedy". It
48+
# had no standing caller at all, which is the defect #10803 records. It has one
49+
# here — as a PREDICATE, never as a verdict.
50+
#
51+
# The two runs below are not redundant, and the order is load-bearing. The gate
52+
# deliberately returns 1 for BOTH "findings, under --strict" and "the instrument
53+
# is broken", so `--strict` alone is ambiguous and could not be branched on:
54+
#
55+
# run 1, ADVISORY — exit 0 means the instrument READ the corpus (findings or
56+
# not); non-zero means it could not, and that is fatal in
57+
# every mode by the gate's own design. This run also
58+
# produces the authored, human-readable finding text.
59+
# run 2, --STRICT — with the instrument already proven healthy by run 1,
60+
# exit 1 now means exactly one thing: findings exist.
61+
#
62+
# That is what lets the patrol branch on a machine-readable boolean instead of
63+
# grepping prose, while the JOB stays green whenever findings exist. A `--strict`
64+
# run stays red for as long as the prose is unwritten — 25 days, in the 16.1.0
65+
# case — and a chronically red scheduled job is how people learn to skim red,
66+
# which `prerelease-pin-watch.yml`'s header warns about directly. Findings land
67+
# in the anchor issue; they never colour this job.
68+
#
69+
# ## What lands where
70+
#
71+
# One pinned ANCHOR ISSUE, rewritten in place every run (`ANCHOR_ISSUE` below) —
72+
# `half-state-patrol.yml`'s shape and its argument, which applies verbatim here:
73+
# never a comment per run, because a per-run comment stream is a second tracker
74+
# nobody prunes and GitHub's edit history is already the archive this needs.
75+
#
76+
# It fits this finding particularly well. The population is tiny — 5 in-scope GA
77+
# minors exist in total (16.0.0, 16.1.0, 17.0.0, 17.1.0, 17.2.0) — so the anchor
78+
# is quiet almost always, and the remedy is owner-shaped: one person writes one
79+
# curated section. An issue can be assigned. An annotation cannot.
80+
#
81+
# The `Swept` timestamp is the patrol's heartbeat and is refreshed even when the
82+
# findings are unchanged: a timestamp that stops advancing is how a reader learns
83+
# the standing caller died. The run must not "optimize away" that no-op edit.
84+
#
85+
# ## Report-only, and the one thing that is NOT report-only
86+
#
87+
# Findings never fail anything. This job never writes release prose — it cannot,
88+
# and the gate's header argues why at length: a curated section is a judgement
89+
# about what is user-facing, read out of 69 package changelogs, and this gate
90+
# GENERATES NOTHING.
91+
#
92+
# The job DOES fail when the sweep could not run, or when its report could not be
93+
# delivered. That is not a gate on the release pages; it is the patrol reporting
94+
# its own death. Silent non-delivery would leave a stale anchor body that reads
95+
# exactly like a clean corpus — "could not read the input" must never look like
96+
# "input is clean" (#4690), with a heartbeat on it. Failing costs nobody a PR:
97+
# this workflow gates no branch and blocks no queue.
98+
99+
on:
100+
# Nightly. #10268's acceptance criterion is "visible within a day of a minor
101+
# publishing", so the cadence is a day, not a week. Offset from
102+
# half-state-patrol.yml's `37 1,7,13,19` so two patrols never contend for the
103+
# same runner minute.
104+
schedule:
105+
- cron: '19 4 * * *'
106+
workflow_dispatch: {}
107+
# Changes to the patrol itself get exercised before they merge — the same
108+
# posture half-state-patrol.yml keeps. On a pull_request run the sweep still
109+
# executes (that is the point: the flags, the exit-code split and the rendering
110+
# are proven on a real runner), but the anchor write is skipped and the
111+
# rendered body goes to the run's step summary instead. A PR must never rewrite
112+
# the board's pinned view.
113+
pull_request:
114+
paths:
115+
- 'scripts/check-release-section-coverage.mjs'
116+
- '.github/workflows/release-coverage-patrol.yml'
117+
118+
# Least privilege: this job reads the repo and writes exactly one issue BODY.
119+
# `issues: write` is the narrowest scope GitHub offers for that edit; the job
120+
# never uses it for labels, comments, assignees or state, and the gate it calls
121+
# is read-only against the filesystem by construction.
122+
permissions:
123+
contents: read
124+
issues: write
125+
126+
# One patrol at a time. A scheduled run overlapping a manual dispatch would have
127+
# two runs racing to rewrite the same body, and the loser's findings would vanish
128+
# with no trace but an edit-history entry.
129+
concurrency:
130+
group: release-coverage-patrol
131+
cancel-in-progress: false
132+
133+
env:
134+
# The pinned anchor issue whose body this workflow owns.
135+
#
136+
# Resolution: the repository variable `RELEASE_COVERAGE_ANCHOR_ISSUE` if set,
137+
# else this repo's own pinned number, else EMPTY — and empty makes the job
138+
# refuse to write rather than guess. The literal is guarded by the repository
139+
# name on purpose: an unguarded fallback would let a verbatim copy in a sibling
140+
# repo rewrite ITS #11879 — some unrelated card — with this repo's findings,
141+
# silently, every night. A number is only meaningful in the repo it was minted
142+
# in.
143+
#
144+
# TO ROTATE (here): open a new `tracking`-labeled issue, put its number below,
145+
# and note the handover in the OLD issue's body before closing it (its edit
146+
# history is the archive and does not travel).
147+
# TO ADOPT (a sibling repo): change NOTHING here — set the repository variable.
148+
#
149+
# ⚠️ Folded scalar, and every continuation line sits at the SAME indent on
150+
# purpose: a more-indented line in a `>-` block keeps its newline literally,
151+
# which would hand the expression parser a multi-line string instead of one
152+
# expression.
153+
ANCHOR_ISSUE: >-
154+
${{ vars.RELEASE_COVERAGE_ANCHOR_ISSUE
155+
|| (github.repository == 'objectstack-ai/objectstack' && '11879')
156+
|| '' }}
157+
158+
jobs:
159+
patrol:
160+
name: Release section-coverage sweep
161+
runs-on: ubuntu-latest
162+
timeout-minutes: 10
163+
steps:
164+
- name: Checkout repository
165+
uses: actions/checkout@v7
166+
167+
- name: Setup Node.js
168+
uses: actions/setup-node@v7
169+
with:
170+
node-version: '22'
171+
172+
# No `pnpm install`: the gate imports `node:fs` and one local module.
173+
# Installing the workspace here would buy nothing and would give a
174+
# scheduled patrol a lockfile it could fail on.
175+
#
176+
# No `--self-test` either. lint.yml runs it on every pull request, so this
177+
# job would be re-proving a property already proven on the commit it just
178+
# checked out — and two node invocations in one `run:` block is precisely
179+
# the shape `check:step-collector` exists to refuse, because the first
180+
# one's failure is masked by the second.
181+
- name: Sweep the corpus
182+
id: sweep
183+
run: |
184+
# ── run 1: ADVISORY. Exit code answers "did the instrument READ the
185+
# corpus", which is the only thing that may colour this job.
186+
set +e
187+
node scripts/check-release-section-coverage.mjs \
188+
> "$RUNNER_TEMP/report.txt" 2> "$RUNNER_TEMP/report.err"
189+
advisory_code=$?
190+
set -e
191+
# Captured with NO pipe in between. `cmd | tail` would report the
192+
# PIPE's status — `tail` essentially never fails, so a healthy and a
193+
# broken instrument would both read as 0.
194+
echo "advisory_code=$advisory_code" >> "$GITHUB_OUTPUT"
195+
echo "advisory run exited $advisory_code"
196+
197+
# ── run 2: --STRICT, used ONLY as a findings predicate. Meaningful
198+
# solely because run 1 has already established the instrument is
199+
# healthy; the gate returns 1 for a broken instrument too.
200+
strict_code=0
201+
if [ "$advisory_code" -eq 0 ]; then
202+
set +e
203+
node scripts/check-release-section-coverage.mjs --strict \
204+
> "$RUNNER_TEMP/strict.txt" 2> "$RUNNER_TEMP/strict.err"
205+
strict_code=$?
206+
set -e
207+
echo "strict run exited $strict_code"
208+
else
209+
echo "instrument is broken — skipping the --strict predicate"
210+
fi
211+
echo "strict_code=$strict_code" >> "$GITHUB_OUTPUT"
212+
213+
cat "$RUNNER_TEMP/report.err" >&2 || true
214+
215+
- name: Resolve the anchor issue
216+
# An install with no anchor configured has nowhere to land its report,
217+
# and the ONLY safe behaviour is to say so loudly. The two alternatives
218+
# are both the failure this file exists to prevent: guessing a number
219+
# would rewrite an unrelated card in this repo, and skipping the write
220+
# quietly would leave a patrol that runs, finds, and tells nobody —
221+
# indistinguishable from a clean corpus.
222+
#
223+
# Placed AFTER the sweep so the run summary still carries the findings,
224+
# and skipped on a pull_request run, which never writes an anchor.
225+
if: github.event_name != 'pull_request'
226+
run: |
227+
if [ -z "${ANCHOR_ISSUE//[[:space:]]/}" ]; then
228+
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)."
229+
exit 1
230+
fi
231+
case "$ANCHOR_ISSUE" in
232+
*[!0-9]*|'') echo "::error::RELEASE_COVERAGE_ANCHOR_ISSUE is '$ANCHOR_ISSUE', which is not an issue number."; exit 1 ;;
233+
esac
234+
echo "anchor: #$ANCHOR_ISSUE in ${{ github.repository }}"
235+
236+
- name: Render the report
237+
id: render
238+
env:
239+
ADVISORY_CODE: ${{ steps.sweep.outputs.advisory_code }}
240+
STRICT_CODE: ${{ steps.sweep.outputs.strict_code }}
241+
PROVENANCE: >-
242+
run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
243+
· commit `${{ github.sha }}` · trigger `${{ github.event_name }}`
244+
run: node scripts/render-release-coverage-anchor.mjs > "$RUNNER_TEMP/body.md"
245+
246+
# The rendered body is the product of the run, so it goes to the step
247+
# summary on EVERY trigger — including the pull_request runs that must not
248+
# touch the anchor. A reader of this workflow's own PR sees exactly what
249+
# would have landed.
250+
- name: Publish the report to the run summary
251+
run: cat "$RUNNER_TEMP/body.md" >> "$GITHUB_STEP_SUMMARY"
252+
253+
- name: Update the pinned anchor issue
254+
# A pull_request run proves the sweep; it must not touch the board.
255+
if: github.event_name != 'pull_request'
256+
uses: actions/github-script@v9
257+
with:
258+
# Delivery is retried, never assumed: this single PATCH is the entire
259+
# product of the run, and a transient answer from the issues endpoint
260+
# would otherwise discard a completed sweep.
261+
retries: 3
262+
script: |
263+
const fs = require('fs');
264+
const path = require('path');
265+
const anchor = Number(process.env.ANCHOR_ISSUE);
266+
const body = fs.readFileSync(path.join(process.env.RUNNER_TEMP, 'body.md'), 'utf8');
267+
if (!body.trim()) {
268+
throw new Error('the renderer produced an empty body — refusing to blank the anchor');
269+
}
270+
await github.rest.issues.update({
271+
owner: context.repo.owner,
272+
repo: context.repo.repo,
273+
issue_number: anchor,
274+
body,
275+
});
276+
core.info(`anchor #${anchor} updated (${body.length} chars)`);
277+
278+
# LAST, so a completed sweep always lands its report before the patrol
279+
# raises the alarm about itself — "land the truth, then raise the alarm".
280+
# This is the ONLY step that may redden the job, and findings can never
281+
# reach it: it reads the instrument's verdict, not the corpus's.
282+
- name: Fail if the sweep could not run
283+
if: steps.sweep.outputs.advisory_code != '0'
284+
run: |
285+
echo "::error::check-release-section-coverage could not read the corpus (exit ${{ steps.sweep.outputs.advisory_code }}). The patrol is DOWN — findings, if any, were NOT judged this run. A sweep that could not run must never read as a clean corpus."
286+
exit 1

0 commit comments

Comments
 (0)