mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 17:04:27 +01:00
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>
53 lines
3.8 KiB
TypeScript
53 lines
3.8 KiB
TypeScript
/**
|
|
* `train forget --train <nonce> | --receipt <pr>@<head> [--dry-run]`: remove a
|
|
* train record (its attempts and bisect ledger) or a READY receipt from
|
|
* refs/notes/gstack-trains under an exact lease, then delete the local files
|
|
* (until now a stale record needed a hand-run
|
|
* `git notes remove` plus a file hunt on every machine that ran the train).
|
|
* A rejected lease removes nothing. Other machines that still hold the local
|
|
* files can push the record back with their next write; the output says so.
|
|
*/
|
|
import * as fs from 'node:fs';
|
|
import type { QueueContext } from './ship-queue';
|
|
import { outOk, outRefused, trainEnv, type TrainOut } from './ship-queue-train';
|
|
import { attemptsOf, forgetNotes, loadBisect, loadReady, loadTrain, noteName, readyPath, rebuildFromNotes, trainDir, type AnyRecord } from './ship-queue-train-record';
|
|
|
|
const Q = 'gstack-ship-queue';
|
|
|
|
export function trainForget(ctx: QueueContext, o: { train?: string; receipt?: string; dryRun: boolean }): TrainOut {
|
|
const env = trainEnv(ctx);
|
|
if ('code' in env) return outRefused(env, [], 'none', 'none');
|
|
rebuildFromNotes(ctx.repoRoot, env.remote);
|
|
const records: AnyRecord[] = [];
|
|
const files: string[] = [];
|
|
let what = '';
|
|
if (o.train) {
|
|
const t = loadTrain(o.train);
|
|
if (!t) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `no train record ${o.train} here or in refs/notes/gstack-trains` }, [], `${Q} train status`, 'none');
|
|
if (t.state === 'merging' || t.state === 'gate-pending' || t.state === 'free-pending') {
|
|
return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `train ${t.nonce} is ${t.state}; forget settles nothing that is still running or merging (train prune --stale for a train whose PRs merged elsewhere)` }, [], `${Q} train status --train ${t.nonce}`, trainDir(t.nonce));
|
|
}
|
|
const bisect = loadBisect(t.nonce);
|
|
records.push(t, ...attemptsOf(t), ...(bisect ? [bisect] : []));
|
|
files.push(trainDir(t.nonce));
|
|
what = `train ${t.nonce} (${t.state}; ${t.attempts.length} attempt(s)${bisect ? ', a bisect ledger' : ''})`;
|
|
} else {
|
|
const m = /^#?(\d+)@([0-9a-f]{40,64})$/.exec(o.receipt ?? '');
|
|
if (!m) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: '--receipt takes <pr>@<full head sha>' }, [], `${Q} train forget --receipt <pr>@<sha>`, 'none');
|
|
const r = loadReady(env.repo.keys, m[1]!, m[2]!);
|
|
if (!r) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `no READY receipt for #${m[1]} at ${m[2]!.slice(0, 12)} here or in refs/notes/gstack-trains` }, [], `${Q} train status`, 'none');
|
|
records.push(r);
|
|
files.push(readyPath(r.repo_key, r.pr, r.head));
|
|
what = `READY receipt #${r.pr} at ${r.head.slice(0, 12)} (screen ${r.screen.choice}${r.screen.run ? `, run ${r.screen.run}` : ''})`;
|
|
}
|
|
const names = records.map(noteName);
|
|
const lines = [`FORGET: ${what}`, ...names.map(n => ` note ${n}`), ...files.map(f => ` local ${f}`)];
|
|
if (o.dryRun) return outOk([...lines, 'DRY-RUN: nothing removed'], `${Q} train forget ${o.train ? `--train ${o.train}` : `--receipt ${o.receipt}`}`, 'none');
|
|
const f = forgetNotes(ctx.repoRoot, names, env.remote);
|
|
if (!f.pushed) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: `the notes ref was not updated (${f.detail}); nothing deleted locally` }, lines, `${Q} train forget ${o.train ? `--train ${o.train}` : `--receipt ${o.receipt}`} (the lease is re-read each run)`, 'none');
|
|
for (const file of files) fs.rmSync(file, { recursive: true, force: true });
|
|
lines.push(`FORGOTTEN: ${f.removed.length} note(s) removed${f.missing.length ? `, ${f.missing.length} already absent` : ''}; ${f.detail}; ${files.length} local path(s) deleted`,
|
|
'note: a machine that still holds these files locally can push the record back with its next write; run train forget there too, or clear its $GSTACK_STATE_ROOT/ship-queue copy');
|
|
return outOk(lines, `${Q} train status`, 'none');
|
|
}
|