Skip to content

Commit e6a03e6

Browse files
os-steveclaude
andauthored
ci: publish the platform checklist status pages to the repository wiki on a schedule (#19162)
Fixes #19157 — the **workflow half** (scope item 7), and the last half of that card. Clause-②: **yes** (widening) ⚠️ **Merge order.** The ledger half is a separate draft on `claude/issue-19157-checklist-planned-status`, and it is what adds `pnpm gen:checklist-status`. Merging this file first leaves a scheduled job that fails until that PR lands — loudly, which is the right direction, but avoidable. Please land the ledger PR first. ⛔ **This PR is human-merged**, and not because anything here is governed: `.github/workflows/**` is not on the governed register. The seat token that opened it lacks the `workflow` scope, so it cannot land this file itself. That is why the workflow is alone in its own pull request instead of riding the ledger change. ## What this does A weekly `schedule:` plus `workflow_dispatch` runs `pnpm gen:checklist-status`, publishes its page set to the **repository wiki**, and mirrors the report into the job summary. The page set is the maintainer's 「一个可以人工阅读确认的入口」 for 「平台真的功能清单,以及实现状态」: - one index page `Platform-Checklist` — a row per area, which is a row per definition item: area, active count, planned count, link; - one `Checklist-AREA` page per area listing every item as `id · title · priority · status · personas`, with the `planned` items in their own section **first**. Sixteen pages on the tree as it stands (index + 15 areas). The count is a property of the ledger and is asserted nowhere: the generator writes what it reads. ## Why the wiki, and not a page in this tree Three shapes were ruled out on the card before this one, and each rejection is load-bearing in the file's header: - a `STATUS.md` committed here — a third artifact to keep fresh, whose stale copy reads exactly as authoritative as a current one; - a `check:checklist-status` gate pairing that page against the generator — explicitly ruled out: staleness here is **tolerated, not gated**, the same standing decision that keeps `check:platform-checklist` out of per-PR CI; - a regen-on-edit rule on whoever touches an area JSON — a rule nobody can enforce is a rule that silently stops being followed. What is left is a schedule. The wiki is one stable URL, outside branch protection and the merge queue, so publishing there costs no review and blocks no pull request. This job writes to the wiki **only** — never a commit to this repository, never an issue, never a label. ## Two refusals rather than quiet passes - **Fewer than two pages produced fails the run.** An empty ledger and a generator that stopped reading it render the same zero, and the second must never be published as the platform's capability list. - **An unclonable wiki fails naming the remedy**, instead of skipping the publish. An uninitialised wiki cannot be cloned, and a job that quietly skipped would report success forever while nothing was ever published — absence must be loud. A third guard sits last: a generate step that recorded no exit code at all fails the run, because nothing downstream could tell that apart from `exit_code=0`. Page ownership is the generator's naming rule and nothing else — the publish step deletes `Platform-Checklist.md` and `Checklist-*.md` before copying, so an area dropped from the ledger does not leave its page standing, and a hand-written wiki page is never collateral. ## Measured, and one thing deliberately NOT The generator, its page set and the counts were measured in the sibling PR; what belongs to this file is its own shape: - the YAML parses, and every `run:` block passes `bash -n`; - triggers are exactly `schedule` (weekly, `37 3 * * 1`, minute offset off the hour and off the sibling patrols), `workflow_dispatch`, and a `pull_request` **paths-filtered to this one file** — so it blocks no unrelated pull request, the posture every patrol in this repo keeps. ⛔ No `merge_group:`, ⛔ no `pull_request_target:`; - `permissions: contents: write` and nothing else; `concurrency` keeps one publisher at a time. ⊘ **NOT MEASURED: a green `workflow_dispatch` run.** A workflow can only be dispatched once it exists on the default branch, so this acceptance item cannot be taken before this PR merges — by construction, not by omission. The `pull_request` trigger above exists partly to close that gap early: a run on this PR exercises checkout, Node, pnpm, the generator and the summary rendering on a real runner, and publishes nothing. After merge, one manual `workflow_dispatch` takes the remaining reading: 16 pages on the wiki, index counts equal to the command output. ⚠️ **One environment prerequisite the first run will surface:** the repository wiki must already exist. If it has never been initialised, the publish step fails and names the remedy (open the Wiki tab, create any first page once); every run after that overwrites the generated pages. `skip-changeset`: a workflow file publishes nothing from any released package. --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 215840f commit e6a03e6

2 files changed

Lines changed: 243 additions & 2 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
name: Platform Checklist Status
2+
3+
# Publishes the platform's capability list AND its implementation status — the
4+
# maintainer's 「一个可以人工阅读确认的入口」 for 「平台真的功能清单,以及实现状态」.
5+
#
6+
# ## Why a schedule publishing to the WIKI, and not a page in the tree
7+
#
8+
# The entry point is GENERATED, never hand-written: the source is
9+
# `docs/qa/platform-checklist/areas/*.json`, and `pnpm gen:checklist-status` is
10+
# the only thing that ever states a count. Three shapes were ruled out on the
11+
# card before this one, and each rejection is load-bearing here:
12+
#
13+
# * a `STATUS.md` committed to this tree — a third artifact to keep fresh,
14+
# whose stale copy reads exactly as authoritative as a current one;
15+
# * a `check:checklist-status` gate pairing that page against the generator —
16+
# ⛔ explicitly ruled out: staleness here is TOLERATED, not gated, and an
17+
# unrelated PR is never blocked by checklist drift (the same standing
18+
# decision that keeps `check:platform-checklist` out of per-PR CI);
19+
# * a regen-on-edit rule on whoever touches an area JSON — a rule nobody can
20+
# enforce is a rule that silently stops being followed.
21+
#
22+
# What is left is a schedule. The wiki is one stable URL, outside branch
23+
# protection and the merge queue, so publishing there costs no review and
24+
# blocks no pull request. ⛔ This workflow therefore writes to the WIKI ONLY —
25+
# it never commits to this repository, never opens an issue, never labels
26+
# anything.
27+
#
28+
# ## The page set — one area JSON, one wiki page
29+
#
30+
# One INDEX page (`Platform-Checklist`): a row per area — which is a row per
31+
# definition item — carrying its active count, its planned count and a link.
32+
# Plus one `Checklist-<area>` page per area listing every item as
33+
# `id · title · priority · status · personas`, with the `planned` items in their
34+
# OWN section, FIRST. The count of pages is a property of the ledger and is
35+
# never asserted here: the generator writes what it reads.
36+
#
37+
# ## ⛔ Why this file lands by a human merge
38+
#
39+
# `.github/workflows/**` is not on the governed register, but the seat token
40+
# that opens this PR lacks the `workflow` scope, so the maintainer merges it by
41+
# hand. It is deliberately the ONLY thing in its pull request — the ledger
42+
# change it reports on lands separately, through the merge queue.
43+
#
44+
# ⚠️ ORDERING: `pnpm gen:checklist-status` is added by that sibling PR. Merging
45+
# this file first leaves a scheduled job that fails until the sibling lands —
46+
# loudly, which is the right direction, but it is avoidable by merging the
47+
# ledger PR first.
48+
49+
# This workflow is paths-filtered (the `pull_request:` trigger below), so
50+
# `scripts/pm/dispatch-gates.mjs` requires it to discover a `check:` family or
51+
# say why it has none. It has none, and that is the maintainer's ruling rather
52+
# than an omission: the whole point of publishing on a schedule is that the
53+
# paired gate was refused. The declaration is the one line under this
54+
# paragraph — it owns that line alone, so ⛔ do not wrap its reason or write a
55+
# comment directly beneath it; a continuation line is read as a CUT reason and
56+
# refused by name.
57+
# dispatch-gates: no-check-families -- the only step that runs anything is `pnpm gen:checklist-status`, a GENERATOR that renders the wiki pages; a paired `check:checklist-status` was explicitly ruled out (staleness of this reading entry is tolerated, not gated), so no named local check family exists for this workflow to discover
58+
on:
59+
schedule:
60+
# Weekly, as ruled. The ledger moves at the pace of platform capabilities,
61+
# not of pull requests, and this page is a reading entry rather than an
62+
# alarm — a day of staleness on it costs nothing a reader can trip over.
63+
#
64+
# The minute is offset off the top of the hour ON PURPOSE (scheduled
65+
# workflows queue behind everyone else's `:00` cron) and off the sibling
66+
# patrols' minutes, so two scheduled jobs never contend for one runner
67+
# minute.
68+
- cron: '37 3 * * 1'
69+
# The manual fire — and the smoke test this card is accepted on.
70+
workflow_dispatch: {}
71+
# ⛔ PATHS-FILTERED TO THIS ONE FILE, and that filter is the whole reason this
72+
# trigger may exist. It fires only on a pull request editing this workflow, so
73+
# it blocks no unrelated pull request at all — the posture every patrol in
74+
# this repo keeps. ⛔ Do NOT widen this list: a second path would put this job
75+
# on the critical path of pull requests that have nothing to do with it.
76+
#
77+
# ⛔ A pull_request run NEVER writes to the wiki. It proves the generator, the
78+
# invocation and the rendering on a real runner, and stops there — see the
79+
# `if:` on the publish step.
80+
pull_request:
81+
paths:
82+
- '.github/workflows/checklist-status.yml'
83+
# ⛔ NO `merge_group:` and ⛔ NO `pull_request_target:` here, ever. Neither is
84+
# made safe by a `paths:` filter, and this job holds a token that can write.
85+
86+
# Least privilege. `contents: write` is what pushing to the repository wiki
87+
# needs; this job writes nothing else — no issue, no label, no comment, and no
88+
# commit to the repository itself.
89+
permissions:
90+
contents: write
91+
92+
# One publisher at a time. A scheduled run overlapping a manual dispatch would
93+
# have two clones racing to push the same pages, and the loser would fail on a
94+
# non-fast-forward for no reason a reader could act on.
95+
concurrency:
96+
group: checklist-status-publish
97+
cancel-in-progress: false
98+
99+
env:
100+
# The index page's name, which `docs/qa/platform-checklist/README.md` links
101+
# and the generator writes. Restated here only so the publish step can report
102+
# the URL it wrote; the generator is the authority on the file names.
103+
INDEX_PAGE: Platform-Checklist
104+
105+
jobs:
106+
publish:
107+
# ⛔ This job is NOT a required context and must never become one: it runs on
108+
# no pull request except one editing this file, so a ruleset naming it would
109+
# block every other PR forever on a check that cannot report.
110+
name: Publish the checklist status pages
111+
runs-on: ubuntu-latest
112+
timeout-minutes: 15
113+
steps:
114+
- name: Checkout repository
115+
uses: actions/checkout@v7
116+
117+
# Kept as this job's own step rather than folded into the composite below:
118+
# `scripts/check-node-version.mjs` scans `.github/workflows/*.yml` only and
119+
# reports how many setup-node steps it audited, so a step moved out of
120+
# sight would silently shrink its census.
121+
- name: Setup Node.js
122+
uses: actions/setup-node@v7
123+
with:
124+
node-version: '22'
125+
126+
# `pnpm` itself is the requirement, NOT the workspace. The generator is
127+
# invoked through its PACKAGE SCRIPT (`pnpm gen:checklist-status`), which
128+
# runs its `--self-test` first: an inlined `node scripts/...` copy would
129+
# drop that leg, and the whole product of this job is a page full of
130+
# numbers that nothing downstream disagrees with.
131+
- name: Setup pnpm
132+
uses: ./.github/actions/setup-pnpm
133+
134+
# No `pnpm install`. The generator imports `node:` builtins and one
135+
# repo-local module — no npm dependency — so installing the workspace
136+
# would buy nothing and would give an unattended weekly run a lockfile it
137+
# could fail on.
138+
- name: Generate the status report and the wiki pages
139+
id: gen
140+
run: |
141+
set +e
142+
pnpm gen:checklist-status --out "$RUNNER_TEMP/wiki-pages" \
143+
> "$RUNNER_TEMP/status.out" 2> "$RUNNER_TEMP/status.err"
144+
code=$?
145+
set -e
146+
# Captured with NO pipe in between. `cmd | tail` reports the PIPE's
147+
# status -- `tail` essentially never fails -- so a red generator and a
148+
# green one would both read as 0.
149+
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
150+
echo "pnpm gen:checklist-status exited $code"
151+
if [ "$code" -ne 0 ]; then
152+
cat "$RUNNER_TEMP/status.out" "$RUNNER_TEMP/status.err" 2>/dev/null || true
153+
exit "$code"
154+
fi
155+
pages=$(find "$RUNNER_TEMP/wiki-pages" -maxdepth 1 -name '*.md' | wc -l | tr -d ' ')
156+
echo "pages=$pages" >> "$GITHUB_OUTPUT"
157+
# A refusal, not a pass. "the ledger is empty" and "the generator
158+
# stopped reading it" produce the same zero, and the second one must
159+
# never be published as the platform's capability list.
160+
if [ "$pages" -lt 2 ]; then
161+
echo "gen:checklist-status produced $pages page(s): an index plus one page per area is at least 2."
162+
echo "This is a REFUSAL, not a pass -- publishing here would replace the wiki with a reading nobody took."
163+
exit 1
164+
fi
165+
166+
- name: Mirror the report into the job summary
167+
# Always: this makes the run self-contained for whoever opens it, and on
168+
# a pull_request run (which publishes nothing) it IS the whole product.
169+
if: always()
170+
run: |
171+
{
172+
echo "### Platform checklist status — generator exit ${{ steps.gen.outputs.exit_code }}"
173+
echo
174+
if [ "${{ github.event_name }}" = "pull_request" ]; then
175+
echo "_Wiki write skipped: a pull_request run proves the generator without publishing anything._"
176+
echo
177+
fi
178+
echo '```'
179+
cat "$RUNNER_TEMP/status.out" 2>/dev/null || echo '(no stdout captured)'
180+
cat "$RUNNER_TEMP/status.err" 2>/dev/null || true
181+
echo '```'
182+
} >> "$GITHUB_STEP_SUMMARY"
183+
184+
- name: Publish the pages to the repository wiki
185+
# ⛔ NEVER on a pull_request run, whatever the generator said.
186+
if: steps.gen.outputs.exit_code == '0' && github.event_name != 'pull_request'
187+
env:
188+
WIKI_URL: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git
189+
run: |
190+
set -euo pipefail
191+
# Absence must be LOUD (AGENTS.md "Route & surface ownership" §3): an
192+
# uninitialised wiki cannot be cloned, and a job that quietly skipped
193+
# the publish would report success forever while nothing was ever
194+
# published. Name the remedy instead.
195+
if ! git clone --depth 1 "$WIKI_URL" "$RUNNER_TEMP/wiki" 2> "$RUNNER_TEMP/clone.err"; then
196+
sed -E 's#x-access-token:[^@]*@#x-access-token:***@#g' "$RUNNER_TEMP/clone.err" || true
197+
echo "Could not clone the repository wiki."
198+
echo "REMEDY: the wiki must exist before it can be written to. Open the repository's Wiki tab and create the first page once (any content); every run after that overwrites the generated pages."
199+
exit 1
200+
fi
201+
cd "$RUNNER_TEMP/wiki"
202+
git config user.name 'github-actions[bot]'
203+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
204+
205+
# Remove the pages this generator OWNS and no others: an area deleted
206+
# from the ledger must not leave its page standing, and a hand-written
207+
# wiki page must not be collateral. Ownership is the naming rule the
208+
# generator uses, and nothing else.
209+
find . -maxdepth 1 -type f \
210+
\( -name "${INDEX_PAGE}.md" -o -name 'Checklist-*.md' \) -delete
211+
cp "$RUNNER_TEMP"/wiki-pages/*.md .
212+
213+
if git diff --quiet && git diff --cached --quiet && [ -z "$(git status --porcelain)" ]; then
214+
echo "wiki already matches the ledger — nothing to publish."
215+
exit 0
216+
fi
217+
git add -A
218+
git commit \
219+
-m 'docs(wiki): regenerate the platform checklist status pages' \
220+
-m "Generated by .github/workflows/checklist-status.yml from docs/qa/platform-checklist/areas/*.json at ${GITHUB_SHA}. Do not edit these pages -- edits go to the area JSON."
221+
git push origin HEAD
222+
echo "published ${{ steps.gen.outputs.pages }} page(s) to ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/wiki/${INDEX_PAGE}"
223+
224+
- name: Fail the run if the generator left no reading
225+
# LAST, on purpose, and narrow: the one state that must never render as
226+
# a healthy run is a generate step that finished without recording an
227+
# exit code at all. Nothing downstream could tell that apart from
228+
# `exit_code=0`, and "the ledger was not read" must never publish as
229+
# "the platform has no capabilities".
230+
if: always() && steps.gen.outputs.exit_code == ''
231+
run: |
232+
echo "the generate step recorded no exit code, so this run is NOT a reading about the checklist. See this run's summary."
233+
exit 1

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22173,8 +22173,16 @@ function selfTest() {
2217322173
}
2217422174
const censusRows = liveMarkerCensus.map((r) => `${r.file}:${r.line} ${r.key}`).sort();
2217522175
t(
22176-
`the live tree carries the six declarations this card measured, and no others (${censusRows.join(' · ') || 'none'})`,
22176+
`the live tree carries the seven declarations measured for this census, and no others (${censusRows.join(' · ') || 'none'})`,
2217722177
censusRows.join(' · ') === [
22178+
// Seventh row, added with the declaration it names: `checklist-status.yml`
22179+
// is paths-filtered (its `pull_request:` trigger is filtered to itself) and
22180+
// its only working step invokes the GENERATOR `pnpm gen:checklist-status`,
22181+
// so it discovers no `check:` family and declares why. ⚠️ This roster is the
22182+
// maintenance this pin exists to force: it is NAMED rather than counted
22183+
// precisely so a seventh declaration reddens WITH the six already read
22184+
// printed beside it, and the author adds their row instead of a number.
22185+
'.github/workflows/checklist-status.yml:57 no-check-families',
2217822186
'.github/workflows/merged-branch-reaper.yml:212 no-check-families',
2217922187
'.github/workflows/os-create-smoke.yml:48 no-check-families',
2218022188
'.github/workflows/scaffold-e2e.yml:23 no-check-families',
@@ -22187,7 +22195,7 @@ function selfTest() {
2218722195
const censusCut = liveMarkerCensus.filter((r) => !r.whole).map((r) => `${r.file}:${r.line} ${r.key}`);
2218822196
t(
2218922197
`every live reason on those markers ENDS on its own marker line (cut: ${censusCut.join(', ') || 'none'})`,
22190-
censusCut.length === 0 && liveMarkerCensus.length === 6,
22198+
censusCut.length === 0 && liveMarkerCensus.length === 7,
2219122199
);
2219222200
t(
2219322201
'and every one of them carries a non-empty reason — whole is not the same claim as present, and both are owed',

0 commit comments

Comments
 (0)