Skip to content

Commit f50c394

Browse files
claude[bot]claude
andauthored
fix(devx): pin the regen self-test fixture to the root's packageManager (#16002)
`check-regen-pending.mjs --self-test` replays the deferred-merge sequence on a throwaway repo and spawns its stub gate with `pnpm -s`. The fixture manifest carried no `packageManager`, and Corepack resolves the version from the project it is invoked in: for an unpinned project that is the registry's `latest` dist-tag, re-resolved every run. When `latest` moved to a pnpm major whose CLI rejects `-s`, the "clean" stub exited 2 without running and the gate went red on every job, on trees nobody had touched. The fixture now copies the ROOT manifest's pin, read at self-test time so it can never drift from the pnpm this repo pins, and one case asserts both halves — the value is a real `pnpm@<version>` pin and it equals the root's — so a root manifest that lost the field cannot make the comparison hold vacuously. Production is untouched: the real hook runs `pnpm -s` in this repository, which is pinned. Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf Co-authored-by: Claude <noreply@anthropic.com>
1 parent f7db8f4 commit f50c394

1 file changed

Lines changed: 45 additions & 1 deletion

File tree

scripts/check-regen-pending.mjs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,10 +928,40 @@ function fixtureSelfTest() {
928928
'node -e "console.error(\'stub-gate: PREREQUISITE NOT MET — the dependency yaml is not installed\'); process.exit(3)"',
929929
};
930930

931+
/**
932+
* The pnpm the fixture's stub gate runs under, pinned to the repository's own.
933+
*
934+
* `runCheck` spawns every gate with `pnpm -s`, and CI reaches pnpm through
935+
* Corepack (`.github/actions/setup-pnpm`). Corepack resolves the version from
936+
* the project it is invoked IN, so a fixture carrying no `packageManager` field
937+
* does not inherit this repo's pin and does not get "whatever the machine has"
938+
* either — it gets the registry's `latest` dist-tag, re-resolved on every run.
939+
* That made this throwaway fixture a canary for whatever pnpm major npm had
940+
* published that morning: the day `latest` moved to a major whose CLI rejects
941+
* `-s`, the "clean" stub exited 2 without ever running, and every case that
942+
* reads the stub's own exit code went red on a tree nobody had touched. It is
943+
* the FIXTURE that was unpinned, never production — the real hook runs `pnpm -s`
944+
* in this repository, which is pinned.
945+
*
946+
* Read off the ROOT manifest rather than written as a literal, so the fixture
947+
* cannot drift from the pnpm this repo pins. The case below asserts both halves,
948+
* because a missing field on either side would compare equal and silently
949+
* restore the bug.
950+
*/
951+
const rootPackageManager = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')).packageManager;
952+
931953
const runHook = (gate, args = []) => {
932954
writeFileSync(
933955
join(dir, 'package.json'),
934-
`${JSON.stringify({ name: 'os-regen-fixture', scripts: { 'check:spec-changes': GATE_STUBS[gate] } }, null, 2)}\n`,
956+
`${JSON.stringify(
957+
{
958+
name: 'os-regen-fixture',
959+
packageManager: rootPackageManager,
960+
scripts: { 'check:spec-changes': GATE_STUBS[gate] },
961+
},
962+
null,
963+
2,
964+
)}\n`,
935965
);
936966
// `spawnSync`, not `execFileSync`: every message this script prints goes to
937967
// STDERR, which execFileSync returns only on the failure path — capturing the
@@ -1000,6 +1030,20 @@ function fixtureSelfTest() {
10001030
writeFileSync(marker, `${pendingPath}\n`);
10011031

10021032
const merge = runHook('stale');
1033+
1034+
// The fixture's OWN prerequisite, asserted before any verdict is read out of a
1035+
// gate's exit code: every case below spawns `pnpm -s` inside this directory, so
1036+
// an unpinned fixture grades the registry's `latest` pnpm instead of the one
1037+
// this repository pins, and the failure lands on cases that have nothing to do
1038+
// with pnpm. ⛔ Not `=== rootPackageManager` on its own: were the root manifest
1039+
// to lose the field, both sides would read `undefined`, this case would pass,
1040+
// and the fixture would be back to resolving `latest` in silence.
1041+
const fixtureManifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
1042+
check("the fixture pins the ROOT's packageManager, so Corepack cannot resolve `latest`",
1043+
typeof rootPackageManager === 'string'
1044+
&& /^pnpm@\d/.test(rootPackageManager)
1045+
&& fixtureManifest.packageManager === rootPackageManager);
1046+
10031047
check('a MERGE commit with stale artifacts is ACCEPTED as a deferral', merge.code === 0);
10041048
check(' …and says so — "DEFERRED", not a silent pass', /DEFERRED to the next commit/.test(merge.out));
10051049
check(' …and points at the sanctioned procedure, never at skipping the hook',

0 commit comments

Comments
 (0)