Skip to content

chore(scripts): refuse a workflow expression naming a context its key cannot see - #27242

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-27227-workflow-context-gate
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-27227-workflow-context-gate

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #27227

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:新增仓库门禁 scripts/check-workflow-context-availability.mjs,拒收在 GitHub「上下文可用性」表不允许的工作流键里引用上下文的表达式(如 job 级 env: 里写 runner.temp)。
  • 为什么改:这类写法会让 GitHub 整个拒收 workflow 文件、不建 run,PR 的检查列表因此「看着全绿」而门禁根本没跑;ci(live-e2e): boot the workspace backend and run the live lane as the required Live E2E UI job #27212 就踩过一次。
  • 风险与回滚:只加一个脚本并串进已有的 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

  • New gate 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").
  • Wiring: chained into the SAME check:workflow-status-functions line in package.json, so the existing Lint & Repo Gates step 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 shared package.json line keeps one CI step.

The table is GitHub's, transcribed. CONTEXT_AVAILABILITY is 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, so jobs.*.container.env.* beats jobs.*.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 matrix is allowed in job-level runs-on/env/timeout-minutes/continue-on-error when the job has a strategy.matrix. The table lists matrix for those keys with no such condition, so the gate imposes none: it encodes the table, not a memory of it. The table forbids matrix in job-level if: and strategy, and both refuse.

Tests

node scripts/check-workflow-context-availability.mjs --self-test: 53 assertions in 9 batteries, all through the real scan() path.

  1. The PR ci(live-e2e): boot the workspace backend and run the live lane as the required Live E2E UI job #27212 head 191e9d5e shape is refused. LIVE_BACKEND_DIR: ${{ runner.temp }}/live-backend in job-level env: gives exactly 1 violation. The context is runner, the row is jobs.*.env, and the line points at the value. The annotation is one line.
  2. The same key at step-level env: passes, and the step expression is counted as judged, not skipped.
  3. The issue's minimum grid, runner/steps/job/matrix × job-level env/if/timeout-minutes/runs-on. Each of the 16 cells is asserted against the table. 13 refuse; 3 (matrix in env/timeout-minutes/runs-on) pass.
  4. The matrix exceptions in runs-on, timeout-minutes, continue-on-error and env pass under a strategy.matrix, with all four judged. matrix in a job-level if: still refuses.
  5. Expression reading. These are not contexts: a property after ., 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. An if: without ${{ }} is read as an expression. A name that is no context is refused as unknown.
  6. Special functions. hashFiles() in job-level env: refuses, and in step env:/if: it passes. A status function outside an if: refuses.
  7. Workflow-level rows. secrets in run-name and runner in top-level env: refuse. secrets in top-level env: and jobs in a workflow_call output pass.
  8. Missing input goes red: no directory, an empty directory, an unparseable file, no jobs:, or an unterminated ${{.
  9. The real tree: no input problems, and more than 0 expressions judged.
Teeth: one-shot mutation (not committed)

The mutation widens the jobs.*.env row to also list runner, in a throwaway copy of the script under scripts/ that a trap removes. Proof that it landed: grep -c "secrets, inputs, runner'" gives 1. The mutant --self-test exits 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, and git status is clean.

Acceptance notes

  • Live tree: 0 findings. The gate scanned 30 workflow files and 78 jobs, and judged 1017 expressions. The raw ${{ 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.
  • Not covered: whether the table changes upstream. The header names the source path and read date, and says to change rows and their pins together.

Verification

All readings below are on head 73f898eb, which is c444d24b with origin/main merged 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 reads OK (scanned 30 workflow file(s), 78 job(s); judged 1017 expression(s) …).
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 41 commands. All 41 were run, plus 5 roster gates under scripts/ that the derivation marks with ⛔.
  • --ran reconciliation: ✓ dispatch-gates --ran: 41 derived famil(ies) accounted for — 37 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3).
  • NOT MEASURED, reason PREREQUISITE NOT MET (exit 3): these gates read built 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 gate check:published-readme-exports.
  • No skills/**, no packages/** and no workflow edit. skip-changeset: the PR touches only repo scripts and the root package.json script line, and nothing in it is published.
Run record (42 exit 0 · 5 exit 3)
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 3
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:entry-guard :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:lean-entry-closure :: exit 3
pnpm check:manifest-repository-directory :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:scripts-symbol-anchors :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 3
pnpm check:turbo-task-graph :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:workflow-status-functions :: exit 0
node scripts/check-published-list-mirrors.mjs :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:i18n-stale-fill :: exit 0
pnpm check:published-readme-exports :: exit 3
node scripts/check-dts-references.mjs --self-test :: exit 0
node scripts/check-nul-bytes.mjs :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0

Generated by Claude Code

…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>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 28, 2026
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · domain:devx#2 · head 73f898ebea · 2026-10-28T12:23Z

维护者速读

  • 改了什么:新增仓库门禁 scripts/check-workflow-context-availability.mjs。工作流表达式如果在 GitHub「上下文可用性」表不允许的位置引用上下文(比如 job 级 env: 里写 runner.temp),这个门禁直接拒收。它挂在已有的 check:workflow-status-functions 命令后面,由现有的 lint 步骤一起跑。
  • 为什么改:这种写法会让 GitHub 整份拒收 workflow 文件、不创建 run,PR 的检查列表看上去「全绿」,其实门禁一个都没跑。ci(live-e2e): boot the workspace backend and run the live lane as the required Live E2E UI job #27212 中途就踩过一次,是靠人眼才发现的。
  • 风险与回滚:只新增一个脚本,外加 package.json 里一行串接,不碰任何 workflow 文件,不属于受管面。误报只会让这个门禁本身变红。回滚 = 还原本 PR。
  • 先看:脚本里的 CONTEXT_AVAILABILITY 表(逐行抄自 GitHub 文档源文件,共 34 行)和 rowFor()(取最具体的匹配行)。
  • 两分钟验证:本地跑 pnpm check:workflow-status-functions,最后一行应为 check-workflow-context-availability: OK (… judged … expression(s) …)。
  • 需要你做的:无。席位落地。

席位意见:合入。门禁按 GitHub 的表实现,没有凭记忆写;现有 30 个 workflow 文件 0 误报。席位另造了两种错误写法试它,都被正确拒收。dev 的两个开放问题席位都选 A。

Shape: draft → main, Fixes #27227. 2 files: scripts/check-workflow-context-availability.mjs (+710) and package.json (+1/−1), +711/−1. Labels skip-changeset and dependencies (auto, from package.json); assignee os-justin. Not governed: no .github/workflows/** path. Report 6468683167, claim 6468029898.

Premise measured: holds at origin/main. scripts/check-workflow-status-functions.mjs reads no context-availability rule, and no actionlint invocation exists in the repo (the claim's premise). The dev re-read it at 835b2d1c.

Seat's own read and probe (at TIER_DEFAULT, on a detached checkout of the PR head):

  • --self-test: 53 assertions, exit 0. Live tree: OK (scanned 30 workflow file(s), 79 job(s); judged 1037 expression(s)), 0 findings. The tree carries ci(live-e2e): boot the workspace backend and run the live lane as the required Live E2E UI job #27212's live-e2e-ui job, whose runner.temp is now at step level.
  • Probe, a hand-written workflow in a temp root. Job-level timeout-minutes: ${{ steps.x.outputs.y }} is refused (steps). Job-level if: hashFiles('a') != '' is refused (function). ${{ runner.temp }} in a step run: passes. fromJSON in step env: passes. These match the table.
  • The walk keeps the key path through sequences, so step rows such as jobs.*.steps.env match. A key with no table row (uses, permissions) is left unjudged, which is conservative. A non-string scalar holds no expression, so skipping it loses nothing.
  • Wiring: the existing Lint & Repo Gates step (lint.yml L3253, pnpm check:workflow-status-functions) now runs both scripts. A red from either fails that one step.

Gates: on head 73f898ebea, 47 check-runs: 28 success, 19 skipped, 0 failure, mergeable_state: clean. check-expected-skips --pr 27242: OK, all 19 skips are in main's roster (exit 0). check-governed-merges --pr 27242: NOT governed, 712 changed lines. The dev's union: pnpm check:workflow-status-functions exit 0, and dispatch-gates --ran 41 families accounted for (37 run, 4 NOT MEASURED by prerequisite, because the diff touches no package).

Open questions, answered by the seat:

  • OQ1 → A, table-faithful: no strategy.matrix precondition on matrix. The defect class is files GitHub rejects outright. GitHub accepts a matrix reference without a matrix (it evaluates empty), so refusing it would be a lint beyond this card. The claim's wording was a gloss, and the table governs.
  • OQ2 → A: the github/docs source file is the page's source of record, and the header names its path and read date.

Accepted deviation: option (b), a new script rather than extending the sibling. The PR body gives the reason: the sibling's header pins its narrow scope. This is within the claim's file surface.

Out of scope: none reported. Skills-docs: none (repo tooling only, no shipped surface).

Landing: non-governed, 712 changed lines, under the 3,000-line floor. The seat readies the PR and arms auto-merge in this act. On merge, the seat verifies the merged diff, posts the landing note on #27227 and archives cloud session session_01QSeKYkorXJzrG4kmSQV2Y7.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 12:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 12:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 295699d Oct 28, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27227-workflow-context-gate branch October 28, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[devx] no repo gate catches a context used where GitHub forbids it — an invalid ci.yml reads as a green PR

2 participants