mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
* 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> * fix(ship-queue): train land takes the shared land lock (train-scoped, freeze-owned), verify --train proves the stamp and needs GitHub MERGED, restamp stages its writes (#3599, #3531) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (train keeps the land lock through a mid-train refusal; freeze takeover reads the notes ref; a refused lock push is not reported as a taken lock) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: a freeze lock is taken over only on a freeze record cleared after it (pre-landing review pass 2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: a stale restamp staging marker is dropped, not a failure; a failed dry run refuses before the real run; settleLock names the real lock-write failure (Step 11 review) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship-queue): land takes over a cleared freeze lock; deleted lock ref reads absent; unread verify head refuses QUEUE_PROOF_MISSING Step 11 adversarial findings on PR #3737: a freeze lock whose release push failed blocked every standalone land after its freeze cleared (freezeLockCleared shared by land and train land takeover); readLock kept a cached ref deleted on origin; verify --train --no-fetch judged the unstamped pin; the slot call now restores the worktree for an unrequested fifth file. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(changelog): name the fragment after the branch slug Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(captain): the slot restamp stages the stamp files it wrote Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship-queue): verify --train refuses QUEUE_PROOF_MISSING on an unread head 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>
65 lines
3.4 KiB
TypeScript
65 lines
3.4 KiB
TypeScript
/** Train setups shared by the test/ship-queue-train*.test.ts files (moved from test/ship-queue-train.test.ts, GSTA-151). */
|
|
import { expect } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { mergeChangelogs } from '../../lib/changelog-check';
|
|
import { EXIT } from '../../lib/headless-artifacts';
|
|
import { runBin } from './restamp-fixture';
|
|
import { GREEN_GATE, line, nonceOf, openPr, queue, readyReceipt, type Rule, type TrainFixture } from './ship-queue-train-fixture';
|
|
|
|
export function twoPrTrain(fx: TrainFixture, gate: Rule = GREEN_GATE) {
|
|
const h1 = openPr(fx, '1');
|
|
const h2 = openPr(fx, '2');
|
|
readyReceipt(fx, '1', h1);
|
|
readyReceipt(fx, '2', h2);
|
|
fx.write(d => { d.rules['evals.yml'] = gate; });
|
|
const r = queue(fx, ['train', 'build', '--prs', `1@${h1},2@${h2}`]);
|
|
return { h1, h2, r, nonce: nonceOf(r.out) };
|
|
}
|
|
|
|
/** A main commit with a prefix's tree plus a VERSION stamp, the way stamp-at-merge publishes it. */
|
|
export function publishMain(fx: TrainFixture, prefixCommit: string, version: string, extra?: { file: string; text: string }): string {
|
|
const { repo } = fx;
|
|
repo.git('checkout', '-q', '-B', 'mainsim', 'origin/main');
|
|
repo.git('rm', '-rq', '.');
|
|
repo.git('checkout', prefixCommit, '--', '.');
|
|
fs.writeFileSync(path.join(repo.work, 'VERSION'), version + '\n');
|
|
if (extra) fs.writeFileSync(path.join(repo.work, extra.file), extra.text);
|
|
repo.git('add', '-A');
|
|
repo.git('commit', '-q', '-m', `merge to ${version}`);
|
|
repo.git('push', '-q', 'origin', 'HEAD:refs/heads/main');
|
|
repo.git('fetch', '-q', 'origin');
|
|
return repo.git('rev-parse', 'HEAD');
|
|
}
|
|
|
|
/** The lane's slot stamp on its pin (main merged in first from slot 2 on): gstack-restamp --next, then the stamp files committed and pushed as the PR head. */
|
|
export function laneStamp(fx: TrainFixture, pr: string, pin: string, o: { mergeMain?: boolean; dropStamp?: boolean } = {}): string {
|
|
const { repo } = fx;
|
|
repo.git('reset', '-q', '--hard');
|
|
repo.git('fetch', '-q', 'origin');
|
|
repo.git('checkout', '-q', '-B', `lane${pr}`, pin);
|
|
if (o.mergeMain) {
|
|
const m = spawnSync('git', ['merge', '--no-ff', '--no-commit', '-q', 'origin/main'], { cwd: repo.work, encoding: 'utf8', timeout: 30_000, env: { ...process.env, ...repo.env } });
|
|
if (m.status !== 0) {
|
|
const side = (n: number) => repo.git('show', `:${n}:CHANGELOG.md`) + '\n';
|
|
fs.writeFileSync(path.join(repo.work, 'CHANGELOG.md'), mergeChangelogs(side(2), side(3), side(1))!);
|
|
repo.git('add', 'CHANGELOG.md');
|
|
}
|
|
}
|
|
const index = repo.git('write-tree');
|
|
const stamp = runBin(repo, 'gstack-restamp', ['--next', '--base', 'main', '--allow-repo-commands'], fx.env);
|
|
expect(stamp.status, stamp.err).toBe(EXIT.ok);
|
|
const files = line(stamp.out, 'FILES:').slice('FILES: '.length).split(' ').filter(Boolean);
|
|
if (o.dropStamp) repo.git('read-tree', index);
|
|
else repo.git('add', '-A', '--', ...files);
|
|
repo.git('commit', '-q', '--no-edit', '-m', `slot stamp #${pr}`);
|
|
if (o.dropStamp) repo.git('checkout', '-q', '--', '.');
|
|
const head = repo.git('rev-parse', 'HEAD');
|
|
repo.git('push', '-q', '-f', 'origin', `${head}:refs/pull/${pr}/head`);
|
|
return head;
|
|
}
|
|
|
|
/** GitHub reports the PR MERGED, which verify --train needs before it confirms a merge (#3531). */
|
|
export const markMerged = (fx: TrainFixture, pr: string) => fx.write(d => { (d.prs[pr] as any).state = 'MERGED'; });
|