Repository navigation
pm(check-expected-skips): derive the expected-skip roster from the workflows' job-level if: gates - #26648
Merged
Conversation
…rkflows' job-level if: gates The roster of jobs allowed to skip was a hand copy of ci.yml's `if:` gates, so a gated job added or renamed in a workflow read exit 4 on every PR its gate said no to until someone added a row. The hand list (EXPECTED_SKIPS) and its audit are gone: deriveGatedSkips reads every job's job-level `if:` and admits it when a top-level `&&` conjunct is a recognised gate term (a `filter` output carrying the `|| 'true'` widening, a per-PR event excluded, a pull_request action excluded, a PR label read), under the raw name its skipped check-run carries. A job with no gate term still reds when it skips; an unwidened, undeclared or un-needed filter output, an unread filter spelling, a shared or untellable name are refused and named. The self-test judges the measured heads against a frozen fixture tree, so no live gated job name is pinned here. Claude-Session: https://claude.ai/code/session_01ADHkmTBKk6ks5yi6zYDatg Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
席位核对ACCEPT ·
Generated by Claude Code |
This was referenced Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Steve · project thread
Fixes #26439
Clause-②: no
Metadata-protocol: none
维护者速读
check-expected-skips不再手抄「允许 skip 的 job 名单」,改为直接读各 workflow 里每个 job 的if:推出名单;手抄的 16 行全部删除。Build UI Deps(new in #25963) is missing from the roster, so every PR whose ui scope is empty reads exit 4 #26111 就是一次),新增或改名一个带门控的 job 都得来这里补一行;本 PR 实测 main 上已经又漏了两个(Plan Test Core shards、Delete the reapable branches)。scripts/pm脚本,不发布、不改 workflow;判错的方向是「多红」不是「放过」——不认识的写法一律拒收并点名。回滚即 revert 本 PR。scripts/pm/check-expected-skips.mjs的gateTerm/jobGate(认哪几种写法)、filterTermProblem(filter 契约三条),再看自测里 ADD / RENAME / UN-GATE 几条。node scripts/pm/check-expected-skips.mjs --roster,预期首行写20 expected-skip name(s), every one derived … none listed by hand,且无What changes
scripts/pm/check-expected-skips.mjsonly.EXPECTED_SKIPS(16 rows),GATE_KINDS,gateSpellings, the reason constants andauditRosterare deleted. The roster is now derived from the tree on every run:rosterFromTree=deriveGatedSkips(new) +deriveNonPrEventSkips(existing shape), off one sharedscanWorkflowJobsscan.deriveGatedSkipsreads each job's job-levelif:, splits it at top-level&&(outside parentheses and literals), and admits the job when at least one conjunct is a recognised gate term:filter-output:needs.filter.outputs.OUTPUT != 'false', or a parenthesised||whose every alternative is one;pr-event:github.event_name != 'pull_request'or!= 'merge_group'(the per-PR events; the full-battery jobs);event-action:github.event.action != 'ACTION'(a labeled / unlabeled / edited run of the same head);label:contains(github.event.pull_request.labels.*.name, 'LABEL')(opt-in) or its!negation (opt-out).name:verbatim (raw matrix template included), so the hard-coded/3,/8,/16shard sizes disappear with the list: GitHub reports a pre-expansion skip under the rawname:, never a size read frommatrix:.--rosternames it): afilterterm whose output lacks the|| 'true'widening, is not declared by thefilterjob, or sits on a job withoutneeds: filter; anif:that readsneeds.filter.outputs.in any spelling the recogniser does not read; a name two jobs carry; a name that cannot be told. A job with no gate term at all is never admitted.judgeCheckRunsandrenderRosternow take the roster explicitly (no hand-list default);--rosterprintsgate: job-gate filter core|crosspkg + pr-event pull_request|merge_group-style lines.self-test-readsdeclaration stays at:137(pinned bydispatch-gates.self-test.mjs) — no edit outside this file.Why
The card: the roster was a hand copy of ci.yml's
if:gates and went red on every new or renamed gated job until someone added a row (#26111:Build UI Deps). Measured onorigin/main7c34e5dc2, the class had already recurred twice more on main:Plan Test Core shardsci.yml›test-plan(gated oncore||crosspkg, added with the shard planner)Delete the reapable branchesmerged-branch-reaper.yml›reap(!= 'pull_request', the workflow runs on PRs touching itself)Tests
The self-test is rewritten in this file (no other test file moves): 173 → 206 cases.
== 'true', a filter term inside a top-level||,(a) || (b),!= 'push',== 'pull_request', double-quoted labels, status functions, two${{ }}blocks, unbalanced parentheses).MEASURED_18315go back to verbatim (Test Core (… /6), the re-spelling to/8is undone).Build UI Deps(new in #25963) is missing from the roster, so every PR whose ui scope is empty reads exit 4 #26111 shape: a new gated job is expected on its first skip, nothing listed; the tree without it reads ⛔), RENAME (new name expected, old name ⛔), DELETE, UN-GATE (if:removed,if: always(), an ungated job added: each ⛔ — a required job silently not run still reds), RE-GATE, the three filter-contract refusals, the unread-spelling refusal, shared / untellable names, an unreadable workflow refused once.if:, no bare status-functionif:admitted, the never-skipping sentinels out, the joined roster frozen and duplicate-free. The CLI cases build their payloads from the live roster.Acceptance notes
--rosterbefore/after on7c34e5dc2: the 18 names before (16 listed + 2 derived) are all present after, plus the two live misses above, 20 in all, 0 refused. The correction: shard sizes are not read frommatrix:blocks; the skipped check-run carries the rawname:, and ci.yml'stestmatrix isfromJSON(needs.test-plan.outputs.shards …), which has no static size to read.TypeScript Type Checkskipped still reds on both trees (below), and the self-test keeps the un-gated cases.reapis now expected. Itsif:conjoinsgithub.event_name != 'pull_request', so its skip on a PR head is by construction; the non-PR-event shape still refuses it (that case is kept).--self-test; unchanged here (not this card), noted only.node scripts/measure-self-test-floor.mjs --self-testexits 1 on this head namingscripts/pm/check-prior-rulings.mjsandscripts/pm/dispatch-gates.mjs(handshake spelling census); neither file is touched here and the script is not a wired check, so it reads the same onmain.Verification
All readings on head
98a3dc80.Acceptance proof on disk: add + rename a gated ci.yml job (one-off, scratch worktrees)
In two scratch worktrees (main
7c34e5dc2and this head), ci.yml was edited:build-docsrenamed toBuild Docs Site, and a new jobBrand New Gated Jobadded withif: ${{ !cancelled() && needs.filter.outputs.core != 'false' }}. Then--self-testand--check-runs-jsonon one-skip payloads:--self-testBuild Docs: ci.yml job build-docs is named "Build Docs Site" — the check-run name moved; correct the rowBrand New Gated JobskippedBuild Docs SiteskippedBuild Docs(the old name) skippedNo file under
scripts/pmwas edited for the PR-side column.The check still reds on the defect it was written for
One-skip payloads,
--check-runs-json, current tree:TypeScript Type Check(no gate)Plan Test Core shardsDelete the reapable branchesBuild UI Deps(the #26111 job)Local gates (dispatch-gates --commands, 31 families)
pnpm check:pm-expected-skips→✓ check-expected-skips self-test: 206 cases passpnpm check:pm-dispatch-gates→ exit 0,✓ dispatch-gates self-test (fast tier): 1935 cases passdispatch-gates --ran→31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED (a DERIVED zero)pnpm check:ci-filter-parityexit 0,pnpm check:required-contextsexit 0 — neither reads this file (git grepfor its name in both scripts: 0 hits), so their output is unchanged by constructioneslint --no-inline-config scripts/pm/check-expected-skips.mjsexit 0;check:nul-bytesexit 0Human Minimum
packages/specholds no runtime logic. (not touched)pnpm lintpasses. (eslint on the file locally; repo-wide in CI)main, one pull request.scripts/pm/**only),skip-changeset; both declaration lines are filled in above.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmTBKk6ks5yi6zYDatg
Generated by Claude Code