Repository navigation
Commit f3f27be
fix(pm): check-expected-skips refuses to judge a live head with a roster that is not the base branch's (#24892)
Fixes #24849
Clause-②: no
## What changed
`scripts/pm/check-expected-skips.mjs` is the only file in the diff.
A live run (`--pr N` / `--head SHA`) now compares the roster in hand
with the base branch's roster **before** it judges anything. When they
differ it answers **exit 3 NOT MEASURED**, naming each differing path
and the re-pin recipe. When they are identical it judges as before, and
the verdict line names whose roster judged it (`... every one in main's
roster (exit 0)`). `--check-runs-json` and `--roster` are unchanged: no
network, no comparison, and the offline verdict line is byte-identical
(pinned).
**What is compared.** The roster is a function of exactly two inputs in
the checkout the command runs from: this file (the listed rows and the
derivation code) and every `.github/workflows/*.yml` (the derived rows).
Their working-tree bytes are hashed in-process as git blob ids, then
compared with the blob ids the contents API lists for `scripts/pm` and
`.github/workflows` at the base branch's **live tip**. That takes two
directory listings and needs no history, no fetch and no git binary.
A refused live run, measured from this branch (its copy of the script is
not main's):
```text
check-expected-skips: NOT MEASURED — the roster in hand is not objectstack-ai/objectstack@main's: 1 of its source file(s) differ from that branch's tip. A stale roster turns a newly rostered skip into a false ⛔ and a retired row's skip into a false OK, so it judges this head neither way
scripts/pm/check-expected-skips.mjs — here d47f540, on main acb32de
re-pin to main's tip, then re-run: git -C /home/user/objectstack-issue-24849 fetch origin main && git -C /home/user/objectstack-issue-24849 checkout --detach origin/main
```
## Refuse or print the gap: the card left this to the script's
maintainer
The choice is to refuse, under the existing exit 3, with no new exit
code. A roster that is not the base's belongs in the same class as an
unreadable head, because it gives no confident verdict in either
direction. A stale roster produces a false ⛔ for a newly rostered job
**and** a false OK for a retired row's skip. Printing the gap beside a 0
or 4 would keep the confident exit code, which is the defect itself. A
distinct exit code would give callers a third NOT MEASURED spelling to
learn. The header's exit register is updated in place.
## Which ref can say whether the roster is current (measured 2026-10-20)
| candidate | verdict |
|---|---|
| checkout `HEAD` behind `origin/main` (ancestry or behind-count) |
**Rejected.** It would refuse on about 98% of merges, none of which can
move the roster: main had 792 first-parent commits in the 7 days to
`b1676441b`, only 14 of them touching `.github/workflows` and 1 touching
this file (`scripts/pm/git-history.mjs count`; the history floor of
2026-09-06 predates the window). `merge-base` and `rev-list` also answer
green from a shallow clone. And a local `origin/main` is only as fresh
as the last fetch. |
| the PR's `base.sha` | **Rejected.** It does not follow the base. PR
#24771 reads `base.sha` = `580fd4ed3` (committed 04:15Z). Its head
`7a9bc58c6` was pushed at 08:05:59Z, and its 08:06Z runs were built on a
main that already carried `ab9890169`. Measured against that `base.sha`,
the stale `6b222464c` would have read as ahead of its base: a false
green on the instance itself. |
| **the base branch's live tip, by blob identity of the roster's
sources** | **Chosen.** Equal sources give an equal roster by
construction, and no history is read, so a shallow clone has nothing to
get wrong. |
`--head` without a PR compares with the board's default branch, at the
cost of one extra `GET /repos/{repo}` read. A different board, whose
base carries neither this file nor these workflows, is now refused
instead of being judged against objectstack's roster. That matches the
pm-dispatch enqueue bar, which already says the roster judges
objectstack only (`名单 check-expected-skips.mjs 只判 objectstack`). The
header paragraph that described cross-board judging is rewritten to
match.
## Evidence
- **The instance, reproduced offline** with `--check-runs-json` on PR
#24771's head `7a9bc58c6` (all 37 of its check-runs, read once into a
fixture file):
- the script as of `6b222464c` exits 4 with `⛔ 1 skipped check-run(s)
outside the roster`, naming `18.x upgrade harness (opt-in)`;
- the script as of `c56115bf9` exits 0 with `10 skipped check-run(s),
every one in the roster`.
- **The comparison, live against main's tip `b1676441b`** (42 roster
sources on main), through the exported functions:
- a snapshot of main's own roster sources: gap 0, at tip;
- the instance's checkout `6b222464c`: gap 2, exactly
`.github/workflows/upgrade-harness-optin.yml — on main (921469d),
absent here` and `scripts/pm/check-expected-skips.mjs — here 6df58d8,
on main acb32de`;
- this branch: gap 1, the script itself.
- **CLI, live.** From this branch, `--pr 24771` and `--head
7a9bc58...` both exit 3 with the gap and the re-pin shown above, at 3
REST reads each.
- NOT MEASURED: the live CLI run where the comparison passes and the
judgement follows. Reason: that needs main to carry these bytes, so it
can only be read after landing. The at-tip half is covered by the gap-0
measurement above and by the self-test.
- **Blob ids agree.** For `scripts/pm/check-expected-skips.mjs` at main,
the contents-API `sha` and `git rev-parse origin/main:PATH` both read
`acb32de67...`.
## Tests
`node scripts/pm/check-expected-skips.mjs --self-test` stays fully
offline. It now runs 169 cases, up from 154, and exits 0 at `fbd5fa30b`.
The new cases:
- git blob ids against two known git values;
- the repo-relative self path;
- this checkout's sources are this file plus every scanned workflow;
- **at tip**: equal sources leave no gap;
- **behind** (the PR #24771 shape): the base's new workflow reads
`absent-here` and this file `differs`, nothing else; the reverse reads
`here-only`;
- the refusal lines end on the re-pin recipe, and a wide gap is capped
and counted;
- the false ⛔ a behind roster gives (exit 4) beside the base roster's
exit 0;
- the live verdict line names `main's roster`, while the offline line is
byte-for-byte the old one;
- the offline CLI prints no roster line;
- a structural pin that the live path calls `rosterGap(` before
`readCheckRuns(`.
## Gates (local, at `fbd5fa30b`)
- **Derived set.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, run with no paths (change set taken from
merge base `b1676441b`), derived 33 commands. All 33 ran and exited 0,
with exit codes captured before any pipe. `--ran` printed `✓
dispatch-gates --ran: 33 derived famil(ies) accounted for — 33 run, 0
NOT-MEASURED`.
- **Battery.** `pnpm check:pm-dispatch-gates`, run detached as its
header prescribes: `✓ dispatch-gates self-test (fast tier): 1913 cases
pass; 6 slow section(s) deferred`, exit 0.
- **Suites that name this file.** `pnpm check:pm-half-states` exits 0
(4916 cases). The `dispatch-gates.data.mjs` and
`dispatch-gates.self-test.mjs` pins run inside the battery above. The
header edit adds no lines above the `self-test-reads` marker, so it
stays on line 133, where that self-test pins it.
- **Lint.** `eslint --no-inline-config` on this one file exits 0.
Repo-wide scans are left to CI.
## Acceptance notes
- `node scripts/measure-self-test-floor.mjs` refuses its census (exit 1:
`2 file(s) carry a landed handshake spelling the recogniser reads as
none: scripts/pm/check-prior-rulings.mjs,
scripts/pm/dispatch-gates.mjs`). It refuses identically on base
`3fbef41b4`, and this diff touches none of those three files. The tool
is a measurement with no CI wiring; carrier: none.
- Boundary, documented in the docblock: the comparison is with the
base's tip **as of the run**. A head whose last run predates a roster
change on its base is judged by the newer roster. An added row costs
nothing. A retired row's skip still reads exit 4, which is also the news
that the head's runs are older than its base.
- No changeset: `scripts/pm/**` publishes nothing, so this PR carries
`skip-changeset`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01M82feuetLzrCiBz41BJb7L)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7e40e8a commit f3f27be
1 file changed
Lines changed: 247 additions & 19 deletions
0 commit comments