Files
gstack/lib/ship-queue-post-merge.ts
T
Garry Tanandcapy-ai[bot] 92a9350a32 v1.92.0.0 ship-queue: merge trains for any repo (train gates, every prefix gated, direct/update land, train init/doctor) (GSTA-157 PR 2) (#3692)
* feat(ship): pause /ship on a red base instead of failing (gstack-base-red, conditional receipts) (GSTA-157 PR 1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: base-red shell runner launches bash through runBashSync

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: base-red coverage additions (unreproduced head, unresolved head, readPause, wait unconfigured)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: base-red pause in unattended endings; BASE_RED_UNCONFIGURED covers an invalid base policy

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: base-red coverage pass 2 (write --base-red, summary line, classify usage before fetch, unresolved-head detail)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(ship-queue): merge trains for any repo — train.gates (workflows/command), every prefix gated, generalized train land (direct/update, merge method), base-red trains, train init/doctor, watch --land (GSTA-157 PR 2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (train gates: dispatch bound, lease, rebase, post-merge checks, output tail, caps)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: train land refuses the next slot until the previous slot's post-merge checks are green

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: ship-policy, troubleshooting and captain for train gates review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: changelog fragment for merge trains for any repo

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: keep ship-queue-train-generic out of the Windows subset (gh shim resolves gh.exe there)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-31 21:46:33 +00:00

52 lines
3.8 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. Commit statuses count too: a pending status keeps the
* commit pending, a failed one makes it red (named as a commit status when no
* check run failed). 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; checks?: string[] }
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 named = o.checks ?? [];
const again = `${Q} verify --post-merge ${o.sha} --timeout ${o.timeoutSec}${named.length ? ` --checks ${JSON.stringify(named.join(','))}` : ''}`;
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.none && !/^(pending|unknown) /.test(ci.line) && !ci.checks.some(c => c.status !== 'completed');
const runs = (n: string) => ci.checks.filter(c => c.name === n || c.name.startsWith(`${n} (`));
const absent = named.filter(n => !runs(n).length);
const unsuccessful = named.filter(n => runs(n).some(c => c.status === 'completed' && c.conclusion !== 'success'));
if (settled && ci.green && unsuccessful.length) return outFailed({ code: 'QUEUE_POST_MERGE_RED', message: `the post-merge checks the train gate names did not succeed at ${at}: ${unsuccessful.join(', ')}` }, lines,
`read the run, repair on a new PR or roll back; ${Q} train status names the freeze if the backstop filed one`, 'none');
if (settled && ci.green && !absent.length) return outOk(lines, 'none', 'none');
if (settled && !ci.green) {
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(', ') || 'a commit status (no check run failed)'}` }, 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.none ? `no check run has attached to ${at} yet` : settled ? `the post-merge checks the train gate names never ran at ${at}: ${absent.join(', ')}` : `main's push workflows at ${at} have not settled at the timeout: ${pending.join(', ') || 'a commit status'} still running`;
return outPending({ code: 'QUEUE_TRAIN_PENDING', message }, lines, again, 'none');
}
await sleep(o.pollMs);
}
}