Skip to content

Commit e80df68

Browse files
committed
fix(spec): do not read a shallow checkout as a forged anchor baseRev (#5235)
First CI run on this branch failed every job: `merge-base --is-ancestor` answers "not an ancestor" in a depth-1 checkout, because the anchor's commit is fetched as its own shallow root and there is no history to walk between it and the tip. The ancestry half of the anchor verification therefore rejected a baseRev that demonstrably IS on main, exactly the way the merge-base fallback a few lines above already anticipates for the baseline itself. Ancestry is now judged only where it can be: `rev-parse --is-shallow-repository` gates it, and a shallow run says so and verifies the recorded KEYS alone — the half truncation cannot take away, since the fetched commit's tree is present. A full clone (every dev checkout — where the anchor is regenerated, and where a hand-edit is therefore caught) still checks both. Two tests pin it, using `$GIT_DIR/shallow` to truncate the sandbox repo exactly as `--depth=1` does: a lagging-but-authentic anchor stays green, and a shed line still goes red there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
1 parent d5742b2 commit e80df68

2 files changed

Lines changed: 71 additions & 6 deletions

File tree

‎packages/spec/scripts/build-schemas-check-mode.test.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,58 @@ describe('build-schemas.ts — an in-tree anchor carries the deletion gate offli
729729
},
730730
);
731731

732+
it(
733+
'does not mistake a shallow checkout for a forged baseRev — CI is shallow, and ancestry there is unwalkable',
734+
{ timeout: SPAWN_TIMEOUT_MS },
735+
() => {
736+
// The shape that broke this change's own first CI run. CI checks out
737+
// depth 1, the anchor's commit is fetched as its own shallow root, and
738+
// `merge-base --is-ancestor` then answers "not an ancestor" about a commit
739+
// that plainly is one. Trusting that answer fails every build.
740+
//
741+
// `$GIT_DIR/shallow` is what makes a repository shallow, so writing the
742+
// current tip into it truncates history exactly the way `--depth=1` does —
743+
// no clone, and `git show BASEREV:file` still works, which is what keeps
744+
// the KEYS half of the verification alive here.
745+
const older = head;
746+
const tip = seedBase((s) => s);
747+
seedSurface((s) => s);
748+
seedSurfaceBase(older, (k) => k);
749+
fs.writeFileSync(path.join(sandbox, '.git', 'shallow'), `${tip}\n`);
750+
try {
751+
expect(git('rev-parse', '--is-shallow-repository')).toBe('true');
752+
const { status, output } = run(['--check']);
753+
754+
expect(output).toContain('shallow checkout');
755+
expect(output).not.toContain('NOT an ancestor of');
756+
expect(status).toBe(0);
757+
} finally {
758+
fs.rmSync(path.join(sandbox, '.git', 'shallow'), { force: true });
759+
}
760+
},
761+
);
762+
763+
it(
764+
'still compares the anchor keys in a shallow checkout — the half that survives truncation',
765+
{ timeout: SPAWN_TIMEOUT_MS },
766+
() => {
767+
const older = head;
768+
const tip = seedBase((s) => s);
769+
seedSurface((s) => s);
770+
seedSurfaceBase(older, (k) => k.filter((x) => x !== SHED_FROM_ANCHOR));
771+
fs.writeFileSync(path.join(sandbox, '.git', 'shallow'), `${tip}\n`);
772+
try {
773+
const { status, output } = run(['--check']);
774+
775+
expect(status).toBe(1);
776+
expect(output).toContain('is not the baseline it claims to be');
777+
expect(output).toContain(SHED_FROM_ANCHOR);
778+
} finally {
779+
fs.rmSync(path.join(sandbox, '.git', 'shallow'), { force: true });
780+
}
781+
},
782+
);
783+
732784
it(
733785
'is a committed artifact: --check reports it missing without writing it, gen:schema creates it from the git baseline',
734786
{ timeout: SPAWN_TIMEOUT_MS * 2 },

‎packages/spec/scripts/build-schemas.ts‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -860,14 +860,16 @@ type GitRun = (...args: string[]) => { status: number | null; stdout: string; st
860860
* commit under test controls:
861861
*
862862
* 1. `baseRev` is an ancestor of origin/main — a PR cannot point it at one of
863-
* its own commits, because its own commits are not upstream;
864-
* 2. the recorded keys ARE that commit's `authorable-surface.json` keys.
863+
* its own commits, because its own commits are not upstream. Decidable only
864+
* where history is walkable, so a shallow checkout says so and skips it;
865+
* 2. the recorded keys ARE that commit's `authorable-surface.json` keys. This
866+
* one holds everywhere the object can be read, shallow included.
865867
*
866868
* Together those make hand-editing the anchor pointless: the only way to shed a
867869
* line from it is to shed the line from an already-merged upstream commit, which
868-
* a PR cannot do. A shallow clone may not hold the object; the run then says so
869-
* and continues, because in that environment the merge-base anchor — not this
870-
* file — is what the deletion check ran on anyway.
870+
* a PR cannot do. A shallow clone may not hold the object at all; the run then
871+
* says so and continues, because in that environment the merge-base anchor — not
872+
* this file — is what the deletion check ran on anyway.
871873
*/
872874
function verifyCommittedSurfaceBase(
873875
git: GitRun,
@@ -904,7 +906,18 @@ function verifyCommittedSurfaceBase(
904906
);
905907
return;
906908
}
907-
if (git('merge-base', '--is-ancestor', rev, tip).status !== 0) {
909+
// Ancestry is only decidable where history is WALKABLE. CI checks out shallow
910+
// (depth 1) and the fetch above grafts `rev` as its own shallow root, so
911+
// `merge-base --is-ancestor` answers "not an ancestor" for a commit that
912+
// demonstrably is one — the same truncation the merge-base fallback above
913+
// already accounts for, and it fails the whole build if trusted (caught on this
914+
// change's own first CI run). Ask whether the answer can mean anything first.
915+
if (git('rev-parse', '--is-shallow-repository').stdout.trim() === 'true') {
916+
console.log(
917+
`ℹ️ ${SURFACE_BASE_FILE_NAME}: shallow checkout — cannot walk history to confirm ${short} is\n` +
918+
` on origin/main, so only its recorded keys are verified here (#5235). A full clone checks both.`,
919+
);
920+
} else if (git('merge-base', '--is-ancestor', rev, tip).status !== 0) {
908921
console.error(
909922
`\n❌ ${SURFACE_BASE_FILE_NAME} names a baseRev (${short}) that is NOT an ancestor of\n` +
910923
` origin/main (#5235).\n\n` +

0 commit comments

Comments
 (0)