fix(ship-queue): ready counts the latest run per check name and hints the dispatch route on a conflicting PR; ship-policy follows #3439

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 06:44:01 +00:00
1 parent f5061445b1
commit 66674ca97b
4 files changed
+133 -14413

No files matched your search

+58
View File
@@ -370,6 +370,31 @@ describe('ready reads all of CI at the head (review #3235 P1-5)', () => {
expect(line(pending.r.out, 'ci: ')).toBe(`ci: pending at ${pending.at} (passed=1 failed=0 pending=1 skipped=0)`);
}, 120_000);
test('a superseded run of a check is judged by its latest run, as GitHub\'s rollup counts (gbrain: a cancelled contributor-gate beside later passes)', () => {
const older = { id: 1, name: 'contributor-gate', status: 'completed', conclusion: 'cancelled', completed_at: '2026-10-26T01:00:00Z' };
const newer = { id: 2, name: 'contributor-gate', status: 'completed', conclusion: 'success', completed_at: '2026-10-26T01:10:00Z' };
const other = { id: 3, name: 'free-tests', status: 'completed', conclusion: 'success', completed_at: '2026-10-26T01:05:00Z' };
const ok = readyWith({ check_runs: [older, newer, other] });
expect(ok.r.status).toBe(EXIT.ok);
expect(line(ok.r.out, 'ci: ')).toBe(`ci: green at ${ok.at} (passed=2 failed=0 pending=0 skipped=0)`);
const reversed = readyWith({ check_runs: [newer, older, other] });
expect(line(reversed.r.out, 'ci: ')).toBe(`ci: green at ${reversed.at} (passed=2 failed=0 pending=0 skipped=0)`);
const latestFailed = readyWith({ check_runs: [{ ...older, conclusion: 'success' }, { ...newer, conclusion: 'failure' }, other] });
expect(latestFailed.r.status).toBe(EXIT.refused);
expect(line(latestFailed.r.out, 'ci: ')).toBe(`ci: red at ${latestFailed.at} (passed=1 failed=1 pending=0 skipped=0)`);
const rerunning = readyWith({ check_runs: [{ ...older, conclusion: 'failure' }, { id: 4, name: 'contributor-gate', status: 'in_progress' }, other] });
expect(line(rerunning.r.out, 'ci: ')).toBe(`ci: pending at ${rerunning.at} (passed=1 failed=0 pending=1 skipped=0)`);
const sameTime = readyWith({ check_runs: [{ ...older, id: 9, completed_at: newer.completed_at }, newer, other] });
expect(line(sameTime.r.out, 'ci: ')).toBe(`ci: red at ${sameTime.at} (passed=1 failed=1 pending=0 skipped=0)`);
const unnamed = readyWith({ check_runs: [{ status: 'completed', conclusion: 'cancelled' }, { status: 'completed', conclusion: 'success' }] });
expect(line(unnamed.r.out, 'ci: ')).toBe(`ci: red at ${unnamed.at} (passed=1 failed=1 pending=0 skipped=0)`);
}, 180_000);
test('a commit-status context is never collapsed with a check run of the same name', () => {
const r = readyWith({ check_runs: [{ id: 1, name: 'deploy', status: 'completed', conclusion: 'success', completed_at: '2026-10-26T01:00:00Z' }] }, { state: 'failure', statuses: [{ context: 'deploy', state: 'failure' }] });
expect(line(r.r.out, 'ci: ')).toBe(`ci: red at ${r.at} (passed=1 failed=1 pending=0 skipped=0)`);
}, 120_000);
test('incomplete pagination is unknown, never green', () => {
const { r, at } = readyWith({ total_count: 101, check_runs: hundred });
expect(r.status).toBe(EXIT.refused);
@@ -377,6 +402,39 @@ describe('ready reads all of CI at the head (review #3235 P1-5)', () => {
}, 120_000);
});
describe('ready on a PR that conflicts with the base (CHANGELOG-only conflicts, rule 14)', () => {
const pass = { id: 1, name: 'Free Tests', status: 'completed', conclusion: 'success', completed_at: '2026-10-26T01:00:00Z' };
function readyWith(mergeable: string, checks: unknown) {
const repo = fixture();
const head = makePr(repo, 'pr-a');
const tree = runBin(repo, 'gstack-wtree', []).out.trim();
const pregate = path.join(repo.root, 'pregate.json');
fs.writeFileSync(pregate, JSON.stringify({ schema_version: 1, tree, head, verdict: 'pass', checks: [{ id: 'unit', status: 'pass' }] }));
const gh = ghQueueShim(repo, { prs: { '1': { headRefOid: head, body: '', mergeable, headRefName: 'pr-a' } }, checks: { [head]: checks } });
return { r: queue(repo, ['ready', '--pr', '1', '--pregate', pregate], gh), at: head.slice(0, 12) };
}
test('no CI at the head and a conflict: the refusal names the workflow_dispatch procedure', () => {
const { r, at } = readyWith('CONFLICTING', { check_runs: [] });
expect(r.status).toBe(EXIT.refused);
expect(line(r.out, 'ci: ')).toBe(`ci: none at ${at} (passed=0 failed=0 pending=0 skipped=0)`);
expect(line(r.out, 'ci-hint: ')).toBe(`ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${at}); dispatch the free workflows on the branch (gh workflow run free-tests.yml --ref pr-a && gh workflow run windows-free-tests.yml --ref pr-a), clear the pregate lanes with their run URLs (gstack-pregate clear --remote <lane> --run-url <run>), then rerun ready`);
}, 120_000);
test('dispatched runs green at the head and a conflict: READY, with the hint saying the dispatched runs count', () => {
const { r, at } = readyWith('CONFLICTING', { check_runs: [pass] });
expect(r.status).toBe(EXIT.ok);
expect(line(r.out, 'ci-hint: ')).toBe(`ci-hint: PR conflicts with the base (no merge ref); the runs at ${at} were dispatched on the branch, which counts`);
}, 120_000);
test('no hint without a conflict, and none while the dispatched runs are red or pending', () => {
expect(readyWith('MERGEABLE', { check_runs: [] }).r.out).not.toContain('ci-hint:');
expect(readyWith('MERGEABLE', { check_runs: [pass] }).r.out).not.toContain('ci-hint:');
expect(readyWith('CONFLICTING', { check_runs: [{ ...pass, conclusion: 'failure' }] }).r.out).not.toContain('ci-hint:');
expect(readyWith('CONFLICTING', { check_runs: [{ ...pass, status: 'in_progress' }] }).r.out).not.toContain('ci-hint:');
}, 240_000);
});
describe('printed gate-ahead command (review #3235 P2-1)', () => {
test("next: runs as printed from a checkout whose path holds a space and an apostrophe", () => {
const { repo } = twoLanes({ ...BASE_POLICY, gate_command: 'node -e "process.exit(0)"', min_gstack_version: SHIP_QUEUE_KEYS_VERSION }, "gstack sq o'fx-");