mirror of https://github.com/garrytan/gstack.git
feat(merge): confirm-enqueue subcommand for enqueue-and-return
gstack-merge gains confirm-enqueue: for a queue regime, confirm the PR was actually picked up (Trunk Merge Queue (<base>) check appeared, or GitHub auto-merge enabled) then RETURN, instead of blocking on the landing poll. Writes a lightweight last-enqueue.json marker (no merge SHA). This is the building block for /land's enqueue-and-return default; wait stays for --watch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fa7ced73a8
commit
f05e61a0bf
|
|
@ -8,13 +8,22 @@
|
||||||
// Subcommands:
|
// Subcommands:
|
||||||
// detect [--base B] [--pr N] [--json]
|
// detect [--base B] [--pr N] [--json]
|
||||||
// submit --regime R --pr N [--base B] [--priority P]
|
// submit --regime R --pr N [--base B] [--priority P]
|
||||||
|
// confirm-enqueue --regime R --pr N --base B [--slug S] [--enqueue-timeout S]
|
||||||
// wait --pr N --base B [--regime R] [--timeout S] [--interval S]
|
// wait --pr N --base B [--regime R] [--timeout S] [--interval S]
|
||||||
// [--enqueue-timeout S] [--once]
|
// [--enqueue-timeout S] [--once]
|
||||||
// write-state --pr N --base B --slug SLUG [--regime R] [--sha-timeout S]
|
// write-state --pr N --base B --slug SLUG [--regime R] [--sha-timeout S]
|
||||||
// read-state --slug SLUG --pr N --repo OWNER/NAME [--max-age-ms N] [--json]
|
// read-state --slug SLUG --pr N --repo OWNER/NAME [--max-age-ms N] [--json]
|
||||||
//
|
//
|
||||||
// Contract: detect/wait/read-state never mutate git/PR state. submit and
|
// `submit` enqueues. For a queue regime the merge then happens asynchronously
|
||||||
// write-state are the only state-touching subcommands.
|
// (trunk/GitHub land it), so /land's DEFAULT is submit + confirm-enqueue +
|
||||||
|
// return — the user can queue many PRs and walk away. `--watch` swaps
|
||||||
|
// confirm-enqueue for the blocking `wait` (poll until landed), which is also
|
||||||
|
// what /land-and-deploy uses (it needs the completed merge to deploy).
|
||||||
|
//
|
||||||
|
// Contract: detect/confirm-enqueue/wait/read-state never mutate git/PR state.
|
||||||
|
// submit and write-state are the only state-touching subcommands.
|
||||||
|
// confirm-enqueue writes last-enqueue.json (a queued marker, no merge SHA);
|
||||||
|
// write-state writes last-land.json (a confirmed merge with a SHA).
|
||||||
|
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
||||||
|
|
@ -238,6 +247,87 @@ function submitViaRest(pr: number, base: string, priority?: string): { code: num
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Write last-enqueue.json — a queued marker (no merge SHA yet). */
|
||||||
|
function writeEnqueueState(slug: string, pr: number, repo: string, regime: Regime, base: string): string {
|
||||||
|
const dir = join(homedir(), '.gstack', 'projects', slug);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
const dest = join(dir, 'last-enqueue.json');
|
||||||
|
const tmp = `${dest}.${process.pid}.tmp`;
|
||||||
|
const body = { schema_version: 1, pr, repo, regime, base, queued: true, ts: new Date().toISOString() };
|
||||||
|
writeFileSync(tmp, JSON.stringify(body, null, 2) + '\n', { mode: 0o600 });
|
||||||
|
renameSync(tmp, dest);
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm a queue regime actually picked the PR up, then RETURN (don't wait for
|
||||||
|
* it to land). This is the default for /land on a queue regime — the whole
|
||||||
|
* point of a merge queue is that you don't babysit. Trunk lands it (optimistic
|
||||||
|
* merge / parallel / batching) on its own.
|
||||||
|
*
|
||||||
|
* Confirmed when: PR already MERGED (fast direct-merge-to-main), OR the
|
||||||
|
* "Trunk Merge Queue (<base>)" check appears (trunk), OR GitHub auto-merge is
|
||||||
|
* enabled (github). Times out for trunk if a posted "/trunk merge" was inert
|
||||||
|
* (App not installed / "GitHub commands" off).
|
||||||
|
*/
|
||||||
|
async function cmdConfirmEnqueue(flags: Record<string, string>): Promise<number> {
|
||||||
|
const pr = flags.pr;
|
||||||
|
const base = flags.base;
|
||||||
|
if (!pr || !base) { process.stderr.write('confirm-enqueue: --pr and --base are required\n'); return 2; }
|
||||||
|
const regime = (flags.regime || 'none') as Regime;
|
||||||
|
const enqueueMs = (Number(flags['enqueue-timeout']) || 120) * 1000;
|
||||||
|
const intervalMs = (Number(flags.interval) || 15) * 1000;
|
||||||
|
|
||||||
|
if (regime === 'none') {
|
||||||
|
// Direct merge is synchronous — there is nothing to enqueue.
|
||||||
|
const v = prView(pr) || {};
|
||||||
|
process.stdout.write(`ENQUEUE_NA=none\nPR_STATE=${(v.state || 'OPEN').toUpperCase()}\n`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
let confirmed = false;
|
||||||
|
let how = '';
|
||||||
|
while (Date.now() - start < enqueueMs) {
|
||||||
|
const v = prView(pr) || {};
|
||||||
|
if ((v.state || '').toUpperCase() === 'MERGED') { confirmed = true; how = 'already merged'; break; }
|
||||||
|
if (regime === 'trunk' && findQueueCheck(prChecks(pr), base)) {
|
||||||
|
confirmed = true; how = `${trunkQueueCheckName(base)} check present`; break;
|
||||||
|
}
|
||||||
|
if (regime === 'github' && v.autoMergeRequest != null) {
|
||||||
|
confirmed = true; how = 'GitHub auto-merge enabled'; break;
|
||||||
|
}
|
||||||
|
await sleep(Math.min(intervalMs, 15000));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
if (regime === 'trunk') {
|
||||||
|
process.stderr.write(
|
||||||
|
'TRUNK_ENQUEUE_TIMEOUT — posted "/trunk merge" but no "' + trunkQueueCheckName(base) +
|
||||||
|
'" check appeared. Is the Trunk GitHub App installed and "GitHub commands" enabled?\n',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
process.stderr.write(
|
||||||
|
'ENQUEUE_UNCONFIRMED — GitHub auto-merge is not enabled on the PR after submit. ' +
|
||||||
|
'Check the repo merge-queue / auto-merge settings.\n',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stateFile = '';
|
||||||
|
if (flags.slug) stateFile = writeEnqueueState(flags.slug, Number(pr), repoSlug(), regime, base);
|
||||||
|
|
||||||
|
process.stdout.write(`ENQUEUED=${regime}\nENQUEUE_HOW=${how}\n`);
|
||||||
|
if (regime === 'trunk') {
|
||||||
|
process.stdout.write(`WATCH_CHECK=${trunkQueueCheckName(base)}\nWATCH_DASHBOARD=https://app.trunk.io\n`);
|
||||||
|
} else {
|
||||||
|
process.stdout.write('WATCH_CHECK=GitHub merge queue (on the PR)\n');
|
||||||
|
}
|
||||||
|
if (stateFile) process.stdout.write(`ENQUEUE_FILE=${stateFile}\n`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
async function cmdWait(flags: Record<string, string>): Promise<number> {
|
async function cmdWait(flags: Record<string, string>): Promise<number> {
|
||||||
const pr = flags.pr;
|
const pr = flags.pr;
|
||||||
const base = flags.base;
|
const base = flags.base;
|
||||||
|
|
@ -416,11 +506,12 @@ async function main(): Promise<number> {
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 'detect': return cmdDetect(flags, bools);
|
case 'detect': return cmdDetect(flags, bools);
|
||||||
case 'submit': return cmdSubmit(flags);
|
case 'submit': return cmdSubmit(flags);
|
||||||
|
case 'confirm-enqueue': return cmdConfirmEnqueue(flags);
|
||||||
case 'wait': return cmdWait(flags);
|
case 'wait': return cmdWait(flags);
|
||||||
case 'write-state': return cmdWriteState(flags);
|
case 'write-state': return cmdWriteState(flags);
|
||||||
case 'read-state': return cmdReadState(flags, bools);
|
case 'read-state': return cmdReadState(flags, bools);
|
||||||
default:
|
default:
|
||||||
process.stderr.write('usage: gstack-merge <detect|submit|wait|write-state|read-state> [flags]\n');
|
process.stderr.write('usage: gstack-merge <detect|submit|confirm-enqueue|wait|write-state|read-state> [flags]\n');
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue