Skip to content

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 into
mainfrom
claude/issue-20756-expected-skips-schedule-only-jobs
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20756-expected-skips-schedule-only-jobs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20756
Clause-②: no

What changed

scripts/pm/check-expected-skips.mjs now 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-level if: is a disjunction of github.event_name == 'schedule' / github.event_name == 'workflow_dispatch' terms only, optionally wrapped whole in one ${{ }}. Such a job skips on every pull_request, push and merge_group run 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 --roster both read the joined roster. There is no literal Registry canary entry.

  • Recogniser, not evaluator. nonPrEventGate is 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 closed NON_PR_EVENTS = ['schedule', 'workflow_dispatch']. push and merge_group are refused on purpose. The header's boundary still holds: nothing here evaluates GitHub's expression language or paths: filters.

  • The name rule (skippedCheckRunName). GitHub evaluates a job-level if: 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 no strategy.matrix, or a non-string name: 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:

    • another job anywhere in the tree carries the same check-run name, so a skip of that other job would read expected;
    • the listed roster already carries the name (derive, don't list);
    • the name cannot be told;
    • its workflow cannot be read.

    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-runs listing for PR #20748 (42 runs), Registry canary: ${{ matrix.template }} is skipped in check suite 99276458880 (run 36658032070). It sits beside Scaffold with repo dist = success in the same suite. .github/workflows/scaffold-e2e.yml at origin/main 73155fed declares the job as name: 'Registry canary: ${{ matrix.template }}' / if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch', with no needs.

What the predicate admits on this tree (39 workflow files, 73155fed): 2 jobs, 0 refused.

workflow › job check-run name on: runs on a PR?
scaffold-e2e.yml › registry-canary Registry canary: ${{ matrix.template }} pull_request (paths), schedule, workflow_dispatch never: skipped on every PR run
publish-smoke.yml › registry-canary Registry canary (published latest) workflow_run (Release), schedule, workflow_dispatch never: the workflow has no PR trigger

These job-level if:s read github.event_name and are not admitted, pinned live:

  • release.yml › version-pr: a workflow_dispatch term conjoined with inputs.refresh_version_pr.
  • release.yml › release-integrity / stale-prompts / publish: push, inputs, needs outputs.
  • merged-branch-reaper.yml › reap: success(), != 'pull_request', inputs.dry_run.
  • publish-smoke.yml › resolve and scaffold-e2e.yml › scaffold-local: != 'schedule'. scaffold-local runs on every PR.

Live control (node scripts/pm/check-expected-skips.mjs --pr 20748, read-only):

  • before (73155fed): VERDICT check-expected-skips: ⛔ 1 skipped check-run(s) outside the roster (exit 4), naming Registry canary: ${{ matrix.template }};
  • after (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].
  • A second, unrelated control (--pr 20816, after) still answers OK — 3 skipped … (exit 0).

Pins (pnpm check:pm-expected-skips, 99 → 154 cases)

  • The card's pin. The measured docs(create-objectstack): re-anchor the dead tracker citations in packages/create-objectstack/src to the commits that decided them #20748 listing is carried verbatim as MEASURED_20748. On disk through --check-runs-json it 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.
  • The control. The same head plus a genuinely unexpected skip answers exit 4, naming only that skip: Lint & Repo Gates through the CLI, TypeScript Type Check in-process. The canary's PR-running sibling Scaffold with repo dist, when skipped, also answers 4.
  • Narrowness. github.event_name == 'schedule' || github.event_name == 'pull_request' is not admitted. The synthetic tree's job carrying that if: 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).
  • The derivation's refusals are each driven on a synthetic tree: a shared name, a listed name, an untellable name, an unreadable file.

Ablation (committed first, restore proven). At 3993387d, node scripts/ablation-replace.mjs replaced the recogniser's first line with return null;. On disk: anchor x1 → x0, marker x0 → x1, blob a999480b → 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 HEAD empty, marker count 0, git status --porcelain empty.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands was run with no paths at 3993387d. It derived the same 30 commands the dispatch named. All 30 ran and all 30 answered exit 0, including pnpm check:pm-dispatch-gates (✓ dispatch-gates self-test: 1976 cases pass.) and pnpm check:pm-expected-skips (154 cases). --ran reconciles: 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:

  • eslint's own config resolves for scripts/pm/check-expected-skips.mjs: --print-config, 2 rules, the file is not ignored;
  • --no-inline-config --format json reports 1 file, 0 errors, 0 warnings;
  • the config enables no type-aware linting (no parserOptions.project, no projectService), so this diff cannot move the verdict on any untouched file.

dispatch-gates.mjs pins this file's self-test-reads declaration at line 131. Every edit here keeps that line in place: the one header sentence changed is rewritten in the same line count.

skip-changeset basis: the diff is scripts/pm/check-expected-skips.mjs alone. It sits under the repo root, whose package.json is private: true. None of the 69 non-private package.json files in the tree has a directory that contains scripts/pm/, so the diff publishes nothing. The seat applies the label.

Acceptance notes

  • A residual exit 4 remains on PRs that edit merged-branch-reaper.yml. That workflow's pull_request trigger is path-filtered to itself, and its reap job (Delete the reapable branches) skips on those runs by design. The narrow recogniser refuses its if: by construction (success() plus inputs.dry_run), as the dispatch required. It is noted here and not filed (carrier: none).
  • Expanded matrix names are not rostered, although the dispatch's mechanism note mentioned them. A job reports expanded names only once it has run, and a run is never a job-level skip. Adding them would widen the roster with names that cannot skip by design.
  • This file's --self-test pins no battery floor. It keeps its handshake (selfTestReachedVerdict-equivalent SELF_TEST_VERDICT record) but has no SELF_TEST_BATTERIES floor 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

…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 objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 30, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 10:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 5cd403e Sep 30, 2026
39 of 40 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20756-expected-skips-schedule-only-jobs branch September 30, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants