|
| 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 |
0 commit comments