diff --git a/lib/ship-queue-train-land.ts b/lib/ship-queue-train-land.ts index d8d248529..c3d0e8e92 100644 --- a/lib/ship-queue-train-land.ts +++ b/lib/ship-queue-train-land.ts @@ -56,6 +56,7 @@ export async function trainLand(ctx: QueueContext, o: { train: string; pr: strin }; const rerun = `${Q} train land --train ${t.nonce} --pr ${o.pr}`; const lines = [`TRAIN_LAND: ${t.nonce} slot ${i + 1}/${t.slots.length} #${o.pr} pin ${pin} -> S${s.index} v${s.version} tree ${s.tree}`]; + if (git(root, ['rev-parse', `${s.commit}^{tree}`]).out !== s.tree || !git(root, ['merge-base', '--is-ancestor', s.commit, t.head]).ok) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `the record's S${s.index} is not a commit with tree ${s.tree.slice(0, 12)} on the gated head ${t.head.slice(0, 12)}` }, lines, `${Q} train status --train ${t.nonce}`, run); let main = mainNow(); if ('error' in main) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: main.error }, lines, rerun, run); if (main.tree === s.tree) return confirmMerged(env, t, o.pr, main.sha, s.index, [...lines, `main: ${main.sha} is S${s.index} exactly`]); @@ -105,6 +106,7 @@ export async function trainLand(ctx: QueueContext, o: { train: string; pr: strin if (!m.ok) return outFailed({ code: 'QUEUE_NOT_READY', message: `gh pr merge refused: ${(m.err ?? m.out).trim().slice(0, 200)}` }, lines, rerun, run); main = mainNow(); if ('error' in main) return outPending({ code: 'QUEUE_PROOF_MISSING', message: `merged; ${main.error}` }, lines, rerun, run); + if (main.sha === j.base) return outPending({ code: 'QUEUE_NOT_READY', message: `merged; main still reads ${j.base.slice(0, 12)}, so the merge is not visible yet` }, lines, rerun, run); if (main.tree !== s.tree) return stopped(env, t, 'QUEUE_TRAIN_TREE_MISMATCH', `after the merge main ${main.sha.slice(0, 12)} is not S${s.index} exactly`, main.sha, lines, true); return confirmMerged(env, t, o.pr, main.sha, s.index, [...lines, `MERGED: #${o.pr} as ${main.sha}; main's tree is S${s.index} exactly`]); } diff --git a/lib/ship-queue-train-stamp.ts b/lib/ship-queue-train-stamp.ts index 8c23c108b..4a7d7faee 100644 --- a/lib/ship-queue-train-stamp.ts +++ b/lib/ship-queue-train-stamp.ts @@ -83,8 +83,9 @@ export function stampChain(o: StampChainOptions): { stamped: StampedPrefix[]; li if (linked) return { code: 'QUEUE_TRAIN_STAMP_MISMATCH', message: `S${i + 1} (#${slot.pr}): ${linked} is a symlink in the merged tree; the stamp writes release files only inside the repository` }; const r = restamp({ repoRoot: wt, base: o.base, baseRef: pred, next: true, policyFrom: 'base', allowRepoCommands: o.allowRepoCommands, dryRun: false, now: new Date(`${date}T12:00:00Z`), env: process.env }); if (r.status !== 'stamped') return { code: r.code ?? 'QUEUE_TRAIN_STAMP_MISMATCH', message: `S${i + 1} (#${slot.pr}): restamp ${r.status}: ${r.message ?? ''}`, fix: r.fix }; - git(wt, ['add', '-A']); + if (!git(wt, ['add', '-A']).ok) return { code: 'QUEUE_TRAIN_STAMP_MISMATCH', message: `cannot stage the stamp for S${i + 1}` }; tree = git(wt, ['write-tree']).out; + if (!/^[0-9a-f]{40,64}$/.test(tree)) return { code: 'QUEUE_TRAIN_STAMP_MISMATCH', message: `cannot write the stamped tree for S${i + 1}` }; version = r.newVersion!; } finally { git(o.repoRoot, ['worktree', 'remove', '--force', wt]); diff --git a/test/ship-queue-train-land.test.ts b/test/ship-queue-train-land.test.ts index eaf569e6d..b02e1936a 100644 --- a/test/ship-queue-train-land.test.ts +++ b/test/ship-queue-train-land.test.ts @@ -118,7 +118,7 @@ describe('stamped trains: build and land', () => { expect(line(drift.out, 'QUEUE_TRAIN_FROZEN:')).toMatch(/^QUEUE_TRAIN_FROZEN: train \S+ merged #1/); }, 300_000); - test('a stamp commit that cannot be written pushes nothing and leaves the PR branch in place', () => { + test('a record whose S_i tree is not on the gated chain pushes nothing and leaves the PR branch in place', () => { const { fx, h1, nonce } = stampedTrain(); expect(queue(fx, ['train', 'verdict', '--train', nonce]).status).toBe(EXIT.ok); const file = path.join(fx.state, 'ship-queue', 'trains', nonce, 'train.json'); @@ -126,7 +126,7 @@ describe('stamped trains: build and land', () => { t.stamped[0].tree = 'f'.repeat(40); fs.writeFileSync(file, JSON.stringify(t)); const r = land(fx, nonce, '1'); - expect(r.status).not.toBe(EXIT.ok); + expect(r.err).toMatch(/is not a commit with tree ffffffffffff on the gated head.*\(QUEUE_PROOF_MISSING\)/); expect(r.out).not.toMatch(/^PUSHED:/m); expect(fx.repo.git('--git-dir', fx.repo.origin, 'rev-parse', 'refs/heads/pr-1')).toBe(h1); }, 180_000);