Files
gstack/bin/gstack-release-stamp
T
Garry Tanandcapy-ai[bot] 9e826b2994 feat(release): versions at release under merge: direct (GSTA-258 L2 PR 2) (#3978)
* feat(release): versions at release under merge: direct (gstack-release-stamp open, restamp --release, land --pin on release/<version>; GSTA-258 L2 PR 2)

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

* fix(release): commit only the stamped files, leave no local release branch, reopen replaces the branch; regenerate doctor anchors

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

* fix(release): land a release head only on the main it was stamped on

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

* fix(release): review round — refresh notes before the release freeze check, check manifest/lockfile/CHANGELOG headings and every release output, branch names the stamped version, reuse an open release PR; restamp stays under 800 lines

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

* fix(release): round 2 — a deleted release output is refused, a release that merged onto a moved main is reported, branch-name test

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

* fix(release): released CHANGELOG sections must stay byte-identical, rerun land checks the stamped base, --bump refused with --release, open ignores fork PRs

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

* docs(release): name the release PR where docs said versions are set at every merge

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

* fix(release): a release that deletes a release output is refused by name, before the release tool runs

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

* fix(land): the release rerun reads the pin's parent after fetching the pin; tests for the rerun, the released-section guard and the fork filter

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

* docs(release): a release may not delete a release output; the release land rerun reads the pin's parent after fetching

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

* docs(release): drop the ship-policy.md re-audit sentences (docs risk accepted by GSTA-258; they go into the follow-up)

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-11-04 03:38:56 +00:00

48 lines
2.8 KiB
TypeScript
Executable File

#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-release-stamp — versions at release under merge: direct (docs/ship-policy.md#release-stamp).
*
* gstack-release-stamp open [--base <branch>] [--dry-run]
*
* Stamps a fresh worktree of origin/<base> with gstack-restamp --release (VERSION, package.json version, every
* changelog.d fragment folded into the CHANGELOG, release outputs, stamp_paths), commits it on release/<version>,
* pushes that branch and opens the release PR. The Captain lands it with gstack-ship-queue land --pr <n> --pin <head>;
* the stable cut then tags v<version>. Runs the base policy's release_tool. Logic: lib/release-stamp.ts.
*/
import { spawnSync } from 'node:child_process';
import { EXIT, exitTableHelp, formatResult } from '../lib/headless-artifacts';
import { openRelease } from '../lib/release-stamp';
import { RESULT_CODES } from '../lib/result-codes';
import { resolveBaseBranch } from '../lib/ship-policy';
const HELP = `gstack-release-stamp — open the release PR that stamps versions under merge: direct (docs/ship-policy.md#release-stamp)
open stamp origin/<base> (gstack-restamp --release), push release/<version>, open its PR
--base <branch> base branch (default: origin/HEAD, else main)
--dry-run stamp in a throwaway worktree and print the diff; push and open nothing
Prints RELEASE: <old> -> <new>, the stage table, PUSHED:, RELEASE_PR: and NEXT: (the land --pin command), then
GSTACK_RESULT: skill=release-stamp status=complete|refused|incomplete.
${exitTableHelp()}`;
function main(argv: string[]): number {
if (argv.includes('--help') || argv.includes('-h')) { process.stdout.write(HELP + '\n'); return EXIT.ok; }
if (argv[0] !== 'open') { process.stderr.write(`gstack-release-stamp: the only command is open\n${HELP}\n`); return EXIT.usage; }
const root = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 15_000 });
if (root.status !== 0) { process.stderr.write('gstack-release-stamp: not inside a git repository\n'); return EXIT.usage; }
const repoRoot = root.stdout.trim();
const i = argv.indexOf('--base');
const r = openRelease({ repoRoot, base: resolveBaseBranch(repoRoot, i >= 0 ? argv[i + 1] : undefined), dryRun: argv.includes('--dry-run') });
for (const line of r.lines) process.stdout.write(line + (line.endsWith('\n') ? '' : '\n'));
if (!r.ok) {
process.stderr.write(`gstack-release-stamp: ${r.message}; fix: ${RESULT_CODES[r.code].fix} (${r.code})\n`);
const refused = r.code !== 'RESTAMP_CONFLICT';
process.stdout.write(formatResult('release-stamp', refused ? 'refused' : 'incomplete', '-') + '\n');
return refused ? EXIT.refused : EXIT.fail;
}
process.stdout.write(formatResult('release-stamp', 'complete', r.url ?? r.branch) + '\n');
return EXIT.ok;
}
process.exitCode = main(process.argv.slice(2));