fix(ship-queue): key the latest run on app and name, keep undated runs standalone, quote the hint branch; restore the full CHANGELOG

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:50:13 +00:00
1 parent 66674ca97b
commit 1b7555ba07
4 files changed
+14457 -32

No files matched your search

+14405 -3
View File
File diff suppressed because it is too large. Load diff
+10 -5
View File
@@ -191,14 +191,19 @@ dispatches the free workflows on the branch (`gh workflow run free-tests.yml
clears the requires-remote pregate lanes with those runs
(`gstack-pregate clear --remote <lane> --run-url <run>`) and reruns `ready`,
which passes because the dispatched check runs attach to the commit; the
`ci-hint:` line then says the dispatched runs count. `ready --screen` on such a
`ci-hint:` line then says the dispatched runs count. The `ci-hint:` line is
advisory text for the lane; nothing parses it, and a PR whose head is on a fork
gets a hint that leaves the dispatch to the coordinator. `ready --screen` on such a
PR is `QUEUE_SCREEN_STALE_REF` (there is no merge ref to screen), so it READYs
with `--no-screen` after the skill's own paid cases ran focused.
`ci:` counts the latest run per check name, as GitHub's rollup does: a check
that was re-run or superseded on the same commit (a cancelled run beside a later
pass of the same name) is judged by its newest run; commit-status contexts are
counted one each.
`ci:` counts the latest run per check (one app's check name), as GitHub's
rollup does: a check that was re-run or superseded on the same commit (a
cancelled run beside a later pass of the same name from a second workflow run)
is judged by its newest run (latest `completed_at`, then id; a run still in
progress is newest); two apps reporting the same name stay separate; a run with
no name or no readable `completed_at` counts on its own; commit-status contexts
are counted one each.
It refuses (`QUEUE_NOT_READY`) when the working tree has tracked edits, HEAD
is not the PR's pushed head, CI at that exact head is not green, or the pregate
+24 -18
View File
@@ -81,23 +81,28 @@ function readAllPages(ctx: QueueContext, endpoint: string, key: 'check_runs' | '
}
/**
* The latest run per check name, the way GitHub's rollup counts: a check that was
* The latest run per check, the way GitHub's rollup counts: a check that was
* re-run or superseded (a cancelled run beside a later pass of the same name on
* the same commit) is judged by its newest run alone. Runs without a name each
* stand on their own. Newest is the latest completed_at, then the highest id; a
* run still in progress has no completed_at and is newer than any finished one.
* the same commit, from a second workflow run) is judged by its newest run
* alone. A check is one app's check name, so two apps that both report "build"
* stay separate. Newest is the latest completed_at, then the highest id; a run
* still in progress is newer than any finished one. A run with no name, or a
* completed run with no readable completed_at, is never collapsed and counts on
* its own (fail closed).
*/
export function latestRunPerName<T extends { name?: unknown; id?: unknown; status?: unknown; completed_at?: unknown }>(runs: T[]): T[] {
export function latestRunPerName<T extends { name?: unknown; id?: unknown; status?: unknown; completed_at?: unknown; app?: unknown }>(runs: T[]): T[] {
const latest = new Map<string, T>();
const unnamed: T[] = [];
const at = (r: T) => (r.status !== 'completed' ? Infinity : typeof r.completed_at === 'string' ? Date.parse(r.completed_at) || 0 : 0);
const standalone: T[] = [];
const rank = (r: T) => (r.status !== 'completed' ? Infinity : typeof r.completed_at === 'string' ? Date.parse(r.completed_at) : NaN);
const id = (r: T) => (typeof r.id === 'number' ? r.id : Number(r.id) || 0);
for (const r of runs) {
if (typeof r.name !== 'string' || !r.name) { unnamed.push(r); continue; }
const have = latest.get(r.name);
if (!have || at(r) > at(have) || (at(r) === at(have) && id(r) > id(have))) latest.set(r.name, r);
const app = (r.app as { id?: unknown } | null | undefined)?.id;
if (typeof r.name !== 'string' || !r.name || Number.isNaN(rank(r))) { standalone.push(r); continue; }
const key = `${typeof app === 'number' || typeof app === 'string' ? app : ''}\0${r.name}`;
const have = latest.get(key);
if (!have || rank(r) > rank(have) || (rank(r) === rank(have) && id(r) > id(have))) latest.set(key, r);
}
return [...latest.values(), ...unnamed];
return [...latest.values(), ...standalone];
}
/** CI at one exact SHA: the latest check run per name (as GitHub's rollup counts) and every commit-status context; unreadable or incomplete is unknown, never green. */
@@ -123,11 +128,12 @@ function ciAtHead(ctx: QueueContext, sha: string): { line: string; green: boolea
* its head. The lane dispatches the free workflows on the branch by hand; their
* check runs attach to the commit, so ready reads them like any other.
*/
function conflictHint(mergeable: unknown, ci: { green: boolean; none: boolean }, branch: string | undefined, head: string): string | null {
function conflictHint(mergeable: unknown, ci: { green: boolean; none: boolean }, branch: string | undefined, fork: boolean, head: string): string | null {
if (mergeable !== 'CONFLICTING') return null;
const ref = branch ? `--ref ${branch}` : '--ref <branch>';
if (ci.none) return `ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${head.slice(0, 12)}); dispatch the free workflows on the branch (gh workflow run free-tests.yml ${ref} && gh workflow run windows-free-tests.yml ${ref}), clear the pregate lanes with their run URLs (gstack-pregate clear --remote <lane> --run-url <run>), then rerun ready`;
if (ci.green) return `ci-hint: PR conflicts with the base (no merge ref); the runs at ${head.slice(0, 12)} were dispatched on the branch, which counts`;
const ref = branch && /^[\w./-]+$/.test(branch) ? `--ref ${branch}` : '--ref <branch>';
if (ci.none && fork) return `ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${head.slice(0, 12)}) and its head is on a fork, so the free workflows cannot be dispatched here; the coordinator decides (advisory)`;
if (ci.none) return `ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${head.slice(0, 12)}); dispatch the free workflows on the branch (gh workflow run free-tests.yml ${ref} && gh workflow run windows-free-tests.yml ${ref}), clear the pregate lanes with their run URLs (gstack-pregate clear --remote <lane> --run-url <run>), then rerun ready (advisory)`;
if (ci.green) return `ci-hint: PR conflicts with the base (no merge ref); the runs at ${head.slice(0, 12)} were dispatched on the branch, which counts (advisory)`;
return null;
}
@@ -154,8 +160,8 @@ export function ready(ctx: QueueContext, o: { pr: string; pregate?: string; gate
const head = git(ctx.repoRoot, ['rev-parse', 'HEAD']).out;
const dirty = git(ctx.repoRoot, ['status', '--porcelain', '--untracked-files=no']).out;
if (dirty) return fail({ code: 'QUEUE_NOT_READY', message: `uncommitted tracked changes: ${dirty.split('\n').map(l => l.slice(3)).slice(0, 8).join(', ')}` }, [], run);
const view = ctx.gh(['pr', 'view', o.pr, '--json', 'headRefOid,url,body,mergeable,headRefName']);
let info: { headRefOid?: string; url?: string; body?: string; mergeable?: string; headRefName?: string } = {};
const view = ctx.gh(['pr', 'view', o.pr, '--json', 'headRefOid,url,body,mergeable,headRefName,isCrossRepository']);
let info: { headRefOid?: string; url?: string; body?: string; mergeable?: string; headRefName?: string; isCrossRepository?: boolean } = {};
try { info = view.ok ? JSON.parse(view.out) : {}; } catch {}
if (!info.headRefOid) return fail({ code: 'QUEUE_NOT_READY', message: `cannot read PR #${o.pr}'s head through gh (${(view.err ?? view.out).trim().slice(0, 120)})` }, [], run);
if (info.headRefOid !== head) return fail({ code: 'QUEUE_NOT_READY', message: `HEAD ${head.slice(0, 12)} is not PR #${o.pr}'s pushed head ${info.headRefOid.slice(0, 12)}` }, [], run);
@@ -177,7 +183,7 @@ export function ready(ctx: QueueContext, o: { pr: string; pregate?: string; gate
}
const receipt = extractReceipt(info.body ?? '');
const attempt = currentAttempt(ctx.repoRoot, o.pr);
const hint = conflictHint(info.mergeable, ci, info.headRefName, head);
const hint = conflictHint(info.mergeable, ci, info.headRefName, info.isCrossRepository === true, head);
const lines = [
`READY #${o.pr} ${head} STAMP_VERSION=${version}`,
`pr: ${info.url ?? `#${o.pr}`}`,
+18 -6
View File
@@ -388,7 +388,15 @@ describe('ready reads all of CI at the head (review #3235 P1-5)', () => {
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);
const twoApps = readyWith({ check_runs: [{ ...older, conclusion: 'failure', app: { id: 1 } }, { ...newer, app: { id: 2 } }, other] });
expect(line(twoApps.r.out, 'ci: ')).toBe(`ci: red at ${twoApps.at} (passed=2 failed=1 pending=0 skipped=0)`);
const undated = readyWith({ check_runs: [{ id: 5, name: 'contributor-gate', status: 'completed', conclusion: 'failure' }, newer, other] });
expect(line(undated.r.out, 'ci: ')).toBe(`ci: red at ${undated.at} (passed=2 failed=1 pending=0 skipped=0)`);
const queued = readyWith({ check_runs: [{ ...older, conclusion: 'failure' }, { id: 6, name: 'contributor-gate', status: 'queued' }, other] });
expect(line(queued.r.out, 'ci: ')).toBe(`ci: pending at ${queued.at} (passed=1 failed=0 pending=1 skipped=0)`);
const nullConclusion = readyWith({ check_runs: [{ ...newer, conclusion: null }, other] });
expect(line(nullConclusion.r.out, 'ci: ')).toBe(`ci: red at ${nullConclusion.at} (passed=1 failed=1 pending=0 skipped=0)`);
}, 240_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' }] });
@@ -404,13 +412,13 @@ describe('ready reads all of CI at the head (review #3235 P1-5)', () => {
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) {
function readyWith(mergeable: string, checks: unknown, pr: Record<string, 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 } });
const gh = ghQueueShim(repo, { prs: { '1': { headRefOid: head, body: '', mergeable, headRefName: 'pr-a', ...pr } }, checks: { [head]: checks } });
return { r: queue(repo, ['ready', '--pr', '1', '--pregate', pregate], gh), at: head.slice(0, 12) };
}
@@ -418,13 +426,17 @@ describe('ready on a PR that conflicts with the base (CHANGELOG-only conflicts,
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);
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 (advisory)`);
const odd = readyWith('CONFLICTING', { check_runs: [] }, { headRefName: 'x;$(curl evil)' });
expect(line(odd.r.out, 'ci-hint: ')).toContain('gh workflow run free-tests.yml --ref <branch> && gh workflow run windows-free-tests.yml --ref <branch>');
const fork = readyWith('CONFLICTING', { check_runs: [] }, { isCrossRepository: true });
expect(line(fork.r.out, 'ci-hint: ')).toBe(`ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${fork.at}) and its head is on a fork, so the free workflows cannot be dispatched here; the coordinator decides (advisory)`);
}, 240_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`);
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 (advisory)`);
}, 120_000);
test('no hint without a conflict, and none while the dispatched runs are red or pending', () => {