…y cannot see
check-workflow-context-availability transcribes GitHub's context-availability
table (Contexts reference) and refuses any expression under a table key that
names a context or special function the row does not list -- the shape that
makes GitHub reject the whole workflow file and create no run, so the PR's
check list reads green while the gate workflow never ran. Chained into the
existing check:workflow-status-functions script line, so the Lint & Repo
Gates step carries it with no workflow edit.
Claude-Session: https://claude.ai/code/session_01QSeKYkorXJzrG4kmSQV2Y7
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #27227
Clause-②: no
Metadata-protocol: none
维护者速读
scripts/check-workflow-context-availability.mjs,拒收在 GitHub「上下文可用性」表不允许的工作流键里引用上下文的表达式(如 job 级env:里写runner.temp)。check:workflow-status-functions命令,不碰 workflow;误报只会让该门禁红,回滚即删脚本与 package.json 里那一段。scripts/check-workflow-context-availability.mjs的CONTEXT_AVAILABILITY表(逐行抄自 GitHub 文档)与rowFor();其次package.json第 163 行。pnpm check:workflow-status-functions,预期四行全过,最后一行是check-workflow-context-availability: OK (... judged 1017 expression(s) ...)。What changes
scripts/check-workflow-context-availability.mjs, with its own--self-test(battery roster + floor + verdict handshake, copied from the sibling per AGENTS.md "Writing a--self-test").check:workflow-status-functionsline inpackage.json, so the existingLint & Repo Gatesstep runs it. No.github/workflows/**edit; the PR stays off the governed surface.Where it lives — option (b), a new script, and why. The sibling's header draws its scope deliberately narrow (job-level
if:×needs.*.outputs.*, no skip-list) and its self-test pins that boundary. This rule judges every key in a 34-row table, at every depth, in both${{ }}and implicit-if:form. Folding it in would blur the sibling's documented boundary and share one battery roster between two unrelated verdicts. A separate file keeps each header the authority on one rule; the sharedpackage.jsonline keeps one CI step.The table is GitHub's, transcribed.
CONTEXT_AVAILABILITYis the "Context availability" table from the Contexts reference, row for row. It was read from the docs source in github/docs,content/actions/reference/workflows-and-actions/contexts.md, on 2026-10-28. docs.github.com itself is outside this container's network allow-list. The most specific matching row governs, sojobs.*.container.env.*beatsjobs.*.container. The special-function column is encoded too, because "unless listed, a function can be used anywhere".One reading differs from the card's wording. The card says
matrixis allowed in job-levelruns-on/env/timeout-minutes/continue-on-errorwhen the job has astrategy.matrix. The table listsmatrixfor those keys with no such condition, so the gate imposes none: it encodes the table, not a memory of it. The table forbidsmatrixin job-levelif:andstrategy, and both refuse.Tests
node scripts/check-workflow-context-availability.mjs --self-test: 53 assertions in 9 batteries, all through the realscan()path.191e9d5eshape is refused.LIVE_BACKEND_DIR: ${{ runner.temp }}/live-backendin job-levelenv:gives exactly 1 violation. The context isrunner, the row isjobs.*.env, and the line points at the value. The annotation is one line.env:passes, and the step expression is counted as judged, not skipped.runner/steps/job/matrix× job-levelenv/if/timeout-minutes/runs-on. Each of the 16 cells is asserted against the table. 13 refuse; 3 (matrixinenv/timeout-minutes/runs-on) pass.matrixexceptions inruns-on,timeout-minutes,continue-on-errorandenvpass under astrategy.matrix, with all four judged.matrixin a job-levelif:still refuses.., a'...'literal, a}}inside a literal, function names, and the literals true/false/null. The index spelling still counts, and context names are case-insensitive. Anif:without${{ }}is read as an expression. A name that is no context is refused as unknown.hashFiles()in job-levelenv:refuses, and in stepenv:/if:it passes. A status function outside anif:refuses.secretsinrun-nameandrunnerin top-levelenv:refuse.secretsin top-levelenv:andjobsin aworkflow_calloutput pass.jobs:, or an unterminated${{.Teeth: one-shot mutation (not committed)
The mutation widens the
jobs.*.envrow to also listrunner, in a throwaway copy of the script underscripts/that atrapremoves. Proof that it landed:grep -c "secrets, inputs, runner'"gives 1. The mutant--self-testexits 1 with 6 failures. Five are in battery 1 ("the specimen is flagged once, got 0", …), and one is the grid cell "runner.x in job-level env: expected refuse, got pass". The real script's self-test then prints✓ … 53 assertions, exit 0, andgit statusis clean.Acceptance notes
${{count is 863, 11 of them in comments; the rest of the 1017 are implicit job- and step-if:bodies. An ad-hoc probe found no expression-bearing scalar outside a table key in today's workflows, so the "keys the table does not name are not judged" boundary leaves nothing unjudged on this tree.Verification
All readings below are on head
73f898eb, which isc444d24bwithorigin/mainmerged in. Each exit code was captured before any pipe.pnpm check:workflow-status-functions: exit 0. The sibling's self-test passes 34 assertions and its live scan is OK. This gate's self-test passes 53 assertions, and its live scan readsOK (scanned 30 workflow file(s), 78 job(s); judged 1017 expression(s) …).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 41 commands. All 41 were run, plus 5 roster gates underscripts/that the derivation marks with ⛔.--ranreconciliation:✓ dispatch-gates --ran: 41 derived famil(ies) accounted for — 37 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3).dist/, and this diff touches no package, so no build closure was made. CI's build lanes run them:check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:sourcemap-no-sources-content, and the roster gatecheck:published-readme-exports.skills/**, nopackages/**and no workflow edit.skip-changeset: the PR touches only repo scripts and the rootpackage.jsonscript line, and nothing in it is published.Run record (42 exit 0 · 5 exit 3)
Generated by Claude Code