From 739a37409002365fe2a85c5f701e42bcf1f1e91a Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 29 Oct 2026 08:52:02 +0000 Subject: [PATCH] fix(train-land): a rerun whose head is still the pin re-enters the stamp step after a journaled, unpushed stamp Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- lib/ship-queue-train-land.ts | 2 +- test/ship-queue-train-land.test.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/ship-queue-train-land.ts b/lib/ship-queue-train-land.ts index 475be1697..d8d248529 100644 --- a/lib/ship-queue-train-land.ts +++ b/lib/ship-queue-train-land.ts @@ -70,7 +70,7 @@ export async function trainLand(ctx: QueueContext, o: { train: string; pr: strin const retargeted = (b?: string) => b && b !== ctx.base ? outRefused({ code: 'QUEUE_NOT_READY', message: `#${o.pr} targets ${b}, not ${ctx.base}; train land merges only PRs on the base the train stamped` }, lines, `retarget #${o.pr} to ${ctx.base}, then ${rerun}`, run) : null; const wrongBase = retargeted(info.baseRefName); if (wrongBase) return wrongBase; - if (!j || j.base !== main.sha) { + if (!j || j.base !== main.sha || info.headRefOid !== j.stamp_commit) { const commit = git(root, ['commit-tree', s.tree, '-p', pin, '-p', main.sha, '-m', `v${s.version} stamp: train ${t.nonce} S${s.index} (#${o.pr})`], { ...process.env, ...STAMP_IDENTITY }).out; if (!isSha(commit)) return outFailed({ code: 'QUEUE_LAND_CONFLICT', message: `cannot create the stamp commit for tree ${s.tree.slice(0, 12)}; nothing pushed` }, lines, `${Q} train status --train ${t.nonce}`, run); if (![pin, commit, j?.stamp_commit].includes(info.headRefOid)) return stopped(env, t, 'QUEUE_TRAIN_MOVED', `#${o.pr}'s head is ${info.headRefOid.slice(0, 12)}, not its pin ${pin.slice(0, 12)}`, main.sha, lines, true); diff --git a/test/ship-queue-train-land.test.ts b/test/ship-queue-train-land.test.ts index 68818f998..eaf569e6d 100644 --- a/test/ship-queue-train-land.test.ts +++ b/test/ship-queue-train-land.test.ts @@ -144,6 +144,17 @@ describe('stamped trains: build and land', () => { expect(fx.repo.git('--git-dir', fx.repo.origin, 'rev-parse', 'main^{tree}')).toBe(fx.repo.git('--git-dir', fx.repo.origin, 'rev-parse', stamp + '^{tree}')); }, 300_000); + test('a rerun after the journal but before the stamp push pushes the stamp instead of stopping', () => { + const { fx, h1, nonce } = stampedTrain(); + expect(queue(fx, ['train', 'verdict', '--train', nonce]).status).toBe(EXIT.ok); + const stamp = /^PUSHED: ([0-9a-f]{40})/m.exec(land(fx, nonce, '1').out)![1]!; + fx.repo.git('push', '-q', '-f', 'origin', `${h1}:refs/heads/pr-1`); + const again = land(fx, nonce, '1'); + expect(again.err).not.toContain('QUEUE_TRAIN_MOVED'); + expect(again.out).toContain(`PUSHED: ${stamp}`); + expect(land(fx, nonce, '1').status).toBe(EXIT.ok); + }, 300_000); + test('a PR retargeted off the train base refuses before any push', () => { const { fx, h1, nonce } = stampedTrain(); expect(queue(fx, ['train', 'verdict', '--train', nonce]).status).toBe(EXIT.ok);