Files
gstack/lib/ship-queue-post-merge.ts
T
garrytanandcapy-ai[bot] c75ec3ad5a feat(ship-queue): screens read from the run, train watch/forget/prune --stale, verify --post-merge, ready --dispatch-ci, --stack-tests
Operating merge trains on 2026-10-26 showed the gaps this closes. A dispatched
screen's verdict is read from the run's gate-binding and report-verdict artifacts
(lib/ship-queue-screen.ts) and recorded in the READY receipt: a RED screen refuses
ready under either choice and is never re-dispatched at the same merge SHA
(QUEUE_SCREEN_RED); a run that concluded success with no slice is NOT-RUN
(QUEUE_SCREEN_NOT_RUN), never followed, and counted as unscreened by train build.
When refs/pull/<n>/merge lags or is missing the screen is planned on a scratch merge
pushed as screen/<pr>-<head12> (#3465); QUEUE_SCREEN_STALE_REF means a real
conflict only. train watch --train is the resumable follow command; train prune
--stale settles a merging/green train whose PRs all merged or closed elsewhere;
train forget removes one record with an exact lease on refs/notes/gstack-trains;
verify --post-merge <sha> watches main's push workflows until they settle;
ready --dispatch-ci <wf,…> dispatches the free workflows on a conflicting PR,
waits, and clears the matching requires-remote pregate lanes with the run URLs;
train build --dry-run --stack-tests runs each PR's new test files on its integrated
prefix before anything is dispatched (QUEUE_TRAIN_STACK_RED).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-26 15:30:32 +00:00

44 lines
2.7 KiB
TypeScript

/**
* `verify --post-merge <sha> [--timeout <s>]`: the coordinator's follow
* command after a train merged a PR onto the base branch. The push workflows
* run on the merged commit (free tests, the Windows subset, the main backstop
* that supersedes itself when main moves on), and a background `gh run
* watch` chain dies with the machine that started it. This command keeps no
* state: it reads every check run attached to the sha, waits (polling gh)
* until none is still running or the timeout passes, prints one `CHECK:`
* line per check, and a `POST_MERGE:` summary that is green only when at
* least one check completed and none failed; a neutral or skipped check is
* skipped, never red. Rerun it after an interruption and it continues.
*/
import { ciAtHead, type QueueContext } from './ship-queue';
import { outFailed, outOk, outPending, type TrainOut } from './ship-queue-train';
const Q = 'gstack-ship-queue';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export interface PostMergeOptions { sha: string; timeoutSec: number; pollMs: number }
export async function verifyPostMerge(ctx: QueueContext, o: PostMergeOptions): Promise<TrainOut> {
const deadline = Date.now() + o.timeoutSec * 1000;
const at = o.sha.slice(0, 12);
const again = `${Q} verify --post-merge ${o.sha} --timeout ${o.timeoutSec}`;
for (;;) {
const ci = ciAtHead(ctx, o.sha);
const lines = [`POST_MERGE: ${ci.line.replace(/^(\S+) at ([0-9a-f]+)/, '$2 $1')}`, ...ci.checks.map(c => `CHECK: ${c.name} ${c.status === 'completed' ? c.conclusion ?? 'unknown' : c.status}`)];
const settled = !ci.checks.some(c => c.status !== 'completed') && !ci.line.startsWith('unknown');
if (settled && ci.green) return outOk(lines, 'none', 'none');
if (settled && ci.checks.length) {
const red = ci.checks.filter(c => c.status === 'completed' && !['success', 'skipped', 'neutral'].includes(c.conclusion ?? '')).map(c => c.name);
return outFailed({ code: 'QUEUE_POST_MERGE_RED', message: `main's push workflows at ${at} are red: ${red.join(', ')}` }, lines,
`read the red run, repair on a new PR or roll back; ${Q} train status names the freeze if the backstop filed one`, 'none');
}
if (Date.now() >= deadline) {
const pending = ci.checks.filter(c => c.status !== 'completed').map(c => c.name);
const message = ci.line.startsWith('unknown') ? `the check runs at ${at} are unreadable (${ci.line.replace(/^unknown at [0-9a-f]+ /, '')})`
: ci.checks.length ? `main's push workflows at ${at} have not settled at the timeout: ${pending.join(', ')} still running` : `no check run has attached to ${at} yet`;
return outPending({ code: 'QUEUE_TRAIN_PENDING', message }, lines, again, 'none');
}
await sleep(o.pollMs);
}
}