fix: pre-landing review fixes (ready at the stamp commit, screen on a stale merge ref, ship-run.json)

- ready accepts the pin's receipt at a slot's stamp commit (plus the slot-2
  merge of main), as verify --train already did; a real edit stays STALE
- ready --screen refuses QUEUE_SCREEN_STALE_REF when refs/pull/<n>/merge is
  missing or merges an older head; a finished screen that planned 0 cases is
  never followed, and train build seats that slot unscreened (GSTA-95 on #3431)
- gstack-ship-receipt result writes ship-run.json (a /ship record, not a run
  v1 manifest), validates --head, and prints status=incomplete on gh failures
- bindReceipt takes an optional pr and renders one-line SHIP_RECEIPT lines
- tests for every state-matrix cell the review named; the train fixture's
  merge ref is a real merge commit

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 02:07:23 +00:00
1 parent 268b0125a5
commit ec438fe8b1
17 files changed
+368 -44

No files matched your search

+37
View File
@@ -295,6 +295,43 @@ describe('ready', () => {
fs.appendFileSync(path.join(repo.work, 'pr-a.txt'), 'dirty\n');
expect(queue(repo, ['ready', '--pr', '1'], shim(moved, green)).err).toMatch(/uncommitted tracked changes: pr-a\.txt.*\(QUEUE_NOT_READY\)/);
}, 180_000);
// Value: protects=the re-READY after a slot's stamp push accepts the receipt /ship wrote for the pinned head, and only then;
// fails_when=ready binds the receipt to HEAD alone (every stamped READY turns QUEUE_RECEIPT_STALE) or accepts any ancestor's receipt;
// why_new=the READY test above binds receipts to the exact head only; seam=none; no_claim=real gh and CI (shims; unit only)
test('ready at the stamp commit accepts the receipt written for the pinned head; a real edit on top of the pin is stale', () => {
const repo = fixture();
const pin = makePr(repo, 'pr-s');
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.4.0\n');
repo.git('commit', '-qam', 'v1.2.4.0 stamp');
const stamped = repo.git('rev-parse', 'HEAD');
const shim = (prHead: string, body: string) => ghQueueShim(repo, { prs: { '1': { headRefOid: prHead, url: 'https://github.com/o/r/pull/1', body } }, checks: { [prHead]: green } });
const ok = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, stamped)], shim(stamped, shippedBody('1', pin)));
expect([ok.status, ok.err]).toEqual([EXIT.ok, '']);
expect(ok.out.split('\n')[0]).toBe(`READY #1 ${stamped} STAMP_VERSION=1.2.4.0`);
expect(ok.out).toContain(`SHIP_RECEIPT: pr=1 head=${pin.slice(0, 12)} version=- base=main`);
repo.git('checkout', '-q', 'origin/main', '-B', 'main-moves');
fs.writeFileSync(path.join(repo.work, 'other.txt'), 'another PR landed\n');
repo.git('add', 'other.txt');
repo.git('commit', '-qm', 'other PR');
repo.git('push', '-q', 'origin', 'HEAD:main');
repo.git('fetch', '-q', 'origin');
repo.git('checkout', '-q', 'pr-s');
repo.git('merge', '-q', '--no-edit', 'origin/main');
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.5.0\n');
repo.git('commit', '-qam', 'v1.2.5.0 stamp');
const slot2 = repo.git('rev-parse', 'HEAD');
const merged = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, slot2)], shim(slot2, shippedBody('1', pin)));
expect([merged.status, merged.err]).toEqual([EXIT.ok, '']);
expect(merged.out).toContain(`SHIP_RECEIPT: pr=1 head=${pin.slice(0, 12)} version=- base=main`);
repo.git('reset', '-q', '--hard', stamped);
fs.appendFileSync(path.join(repo.work, 'pr-s.txt'), 'real change after /ship\n');
repo.git('commit', '-qam', 'more');
const edited = repo.git('rev-parse', 'HEAD');
const stale = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, edited)], shim(edited, shippedBody('1', pin)));
expect(stale.status).toBe(EXIT.refused);
expect(stale.err).toMatch(new RegExp(`PR #1's ship receipt is for ${pin.slice(0, 12)}, not the head ${edited.slice(0, 12)} being merged.*\\(QUEUE_RECEIPT_STALE\\)`));
}, 180_000);
});
describe('receipt digest', () => {