fix(pm): check-expected-skips derives schedule/dispatch-only jobs as expected skips under their raw name - #20835
Merged
objectstack-fleet[bot] merged 2 commits intoSep 30, 2026
Conversation
…expected skips under their raw name WIP: the derivation and its wiring into the judge and --roster; self-test cases follow. Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <[email protected]>
The recogniser admits only its one exact shape and refuses a pull_request / push / merge_group term, a negation and every live mixed if:; the derivation refuses a shared, a listed and an untellable name; the measured #20748 head judges 0 with its raw-named canary and 4 with a genuinely unexpected skip beside it, both in-process and through the CLI on a payload file. Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <[email protected]>
objectstack-fleet
Bot
deleted the
claude/issue-20756-expected-skips-schedule-only-jobs
branch
September 30, 2026 11:08
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.
Fixes #20756
Clause-②: no
What changed
scripts/pm/check-expected-skips.mjsnow derives a second half of its expected-skip roster from the workflows, next to the eleven listed rows. The listed rows are unchanged. The derived half admits a job if its job-levelif:is a disjunction ofgithub.event_name == 'schedule'/github.event_name == 'workflow_dispatch'terms only, optionally wrapped whole in one${{ }}. Such a job skips on everypull_request,pushandmerge_grouprun by construction. The row takes the check-run name GitHub reports for that skip:name:verbatim, else the job key. The judge (--pr/--head/--check-runs-json) and--rosterboth read the joined roster. There is no literalRegistry canaryentry.Recogniser, not evaluator.
nonPrEventGateis an allow-list of one comparison shape,github.event_name == 'EVENT', joined by||. It returns null on anything else:&&,!=, parentheses,inputs.*,needs.*,success(),matrix.*, a label read, a case-folded or double-quoted literal, two${{ }}blocks, or an event outside the closedNON_PR_EVENTS = ['schedule', 'workflow_dispatch'].pushandmerge_groupare refused on purpose. The header's boundary still holds: nothing here evaluates GitHub's expression language orpaths:filters.The name rule (
skippedCheckRunName). GitHub evaluates a job-levelif:before it expands the matrix, so a gated matrix job's skipped check-run keeps each${{ matrix.* }}literally. Only a bare matrix reference is admitted. A name holding any other expression, a matrix reference on a job with nostrategy.matrix, or a non-stringname:is refused, because its skipped spelling is unmeasured. Expanded names are deliberately not rostered: an expanded job ran, so its check-runs are never a gate skip.Refusals are the safe direction. A candidate stays out of the roster, and so still answers exit 4, when any of these holds:
The live self-test holds the refusal list at empty, so a job's author hears about a refusal before a landing seat does.
Measured
Premise, confirmed. The shape is on the real check-run: in the full
GET /commits/a84b73af13/check-runslisting for PR #20748 (42 runs),Registry canary: ${{ matrix.template }}isskippedin check suite 99276458880 (run 36658032070). It sits besideScaffold with repo dist= success in the same suite..github/workflows/scaffold-e2e.ymlatorigin/main73155feddeclares the job asname: 'Registry canary: ${{ matrix.template }}'/if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch', with noneeds.What the predicate admits on this tree (39 workflow files,
73155fed): 2 jobs, 0 refused.on:scaffold-e2e.yml›registry-canaryRegistry canary: ${{ matrix.template }}publish-smoke.yml›registry-canaryRegistry canary (published latest)These job-level
if:s readgithub.event_nameand are not admitted, pinned live:release.yml›version-pr: aworkflow_dispatchterm conjoined withinputs.refresh_version_pr.release.yml›release-integrity/stale-prompts/publish:push,inputs,needsoutputs.merged-branch-reaper.yml›reap:success(),!= 'pull_request',inputs.dry_run.publish-smoke.yml›resolveandscaffold-e2e.yml›scaffold-local:!= 'schedule'.scaffold-localruns on every PR.Live control (
node scripts/pm/check-expected-skips.mjs --pr 20748, read-only):73155fed):VERDICT check-expected-skips: ⛔ 1 skipped check-run(s) outside the roster (exit 4), namingRegistry canary: ${{ matrix.template }};3993387d):VERDICT check-expected-skips: OK — 8 skipped check-run(s), every one in the roster (exit 0). The canary is listed as[scaffold-e2e.yml › registry-canary; derived].--pr 20816, after) still answersOK — 3 skipped … (exit 0).Pins (
pnpm check:pm-expected-skips, 99 → 154 cases)MEASURED_20748. On disk through--check-runs-jsonit answers exit 0, with the canary tagged derived. In-process against the joined roster it is 0 as well. Against the listed half alone it still names the canary, which shows the derivation is what moved it.Lint & Repo Gatesthrough the CLI,TypeScript Type Checkin-process. The canary's PR-running siblingScaffold with repo dist, when skipped, also answers 4.github.event_name == 'schedule' || github.event_name == 'pull_request'is not admitted. The synthetic tree's job carrying thatif:answers exit 4 when skipped.push,merge_group, negation,inputs,needs,success(),matrix, label, parenthesised, case-folded and double-quoted forms each have their own null case. So do both live mixed shapes (version-pr,reap).Ablation (committed first, restore proven). At
3993387d,node scripts/ablation-replace.mjsreplaced the recogniser's first line withreturn null;. On disk: anchor x1 → x0, marker x0 → x1, bloba999480b→0dc4f77f. The self-test then went red: 22 of 154 failed. Among the red cases:measured #20748: the raw-named skipped canary judges exit 0 … (got 4, want 0);cli: the measured #20748 head on disk (raw-named skipped canary) → exit 0 (got 4, want 0).The control and narrowness cases stayed green, which is the expected direction. The restore was proven: blob after restore == blob at HEAD (
a999480b),git diff HEADempty, marker count 0,git status --porcelainempty.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandswas run with no paths at3993387d. It derived the same 30 commands the dispatch named. All 30 ran and all 30 answered exit 0, includingpnpm check:pm-dispatch-gates(✓ dispatch-gates self-test: 1976 cases pass.) andpnpm check:pm-expected-skips(154 cases).--ranreconciles:30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED.Lint was narrowed to the one touched file and still counts as a measurement:
scripts/pm/check-expected-skips.mjs:--print-config, 2 rules, the file is not ignored;--no-inline-config --format jsonreports 1 file, 0 errors, 0 warnings;parserOptions.project, noprojectService), so this diff cannot move the verdict on any untouched file.dispatch-gates.mjspins this file'sself-test-readsdeclaration at line 131. Every edit here keeps that line in place: the one header sentence changed is rewritten in the same line count.skip-changesetbasis: the diff isscripts/pm/check-expected-skips.mjsalone. It sits under the repo root, whosepackage.jsonisprivate: true. None of the 69 non-privatepackage.jsonfiles in the tree has a directory that containsscripts/pm/, so the diff publishes nothing. The seat applies the label.Acceptance notes
merged-branch-reaper.yml. That workflow'spull_requesttrigger is path-filtered to itself, and itsreapjob (Delete the reapable branches) skips on those runs by design. The narrow recogniser refuses itsif:by construction (success()plusinputs.dry_run), as the dispatch required. It is noted here and not filed (carrier: none).--self-testpins no battery floor. It keeps its handshake (selfTestReachedVerdict-equivalentSELF_TEST_VERDICTrecord) but has noSELF_TEST_BATTERIESfloor as AGENTS.md describes. That was true before this PR; the new cases join its existing sections. Noted, not changed (carrier: none).Generated by Claude Code