fix: under bump never a release PR that moves VERSION takes the Version Gate's ordinary rules

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-31 03:51:15 +00:00
1 parent ee5e2c37dc
commit 83bd272f40
3 files changed
+8 -9

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
| Key | Values | Default | Meaning |
|---|---|---|---|
| `bump` | `patch` \| `ask` \| `never` | `ask` | `patch` skips the MINOR/MAJOR prompt in `/ship` Step 12 and `gstack-restamp --next` bumps PATCH; `ask` keeps the prompt, and in a session no person answers any version Step 12 would write (`--bump` on a fresh branch, `--version <v>` on a bumped one) stops with `BUMP_ANSWER_REQUIRED` until `--answer "<the owner's words>"`; `never` is for a repo whose unit PRs never change VERSION: Step 12 keeps it with no question, refuses `--bump`, and `gstack-restamp --next` needs an explicit `--bump` or `--version` (the release process bumps). `never` needs `min_gstack_version` at its introducing release and cannot be combined with `queue_mode: stamp-at-merge`, `train` or `changelog_fragments`, which stamp a version at every merge. |
| `bump` | `patch` \| `ask` \| `never` | `ask` | `patch` skips the MINOR/MAJOR prompt in `/ship` Step 12 and `gstack-restamp --next` bumps PATCH; `ask` keeps the prompt, and in a session no person answers any version Step 12 would write (`--bump` on a fresh branch, `--version <v>` on a bumped one) stops with `BUMP_ANSWER_REQUIRED` until `--answer "<the owner's words>"`; `never` is for a repo whose unit PRs never change VERSION: Step 12 keeps it with no question, refuses `--bump`, and `gstack-restamp --next` needs an explicit `--bump` or `--version` (the release process bumps). CI's Version Gate passes a head that keeps VERSION; a release PR that moves it takes the ordinary bump and collision rules. `never` needs `min_gstack_version` at its introducing release and cannot be combined with `queue_mode: stamp-at-merge`, `train` or `changelog_fragments`, which stamp a version at every merge. |
| `queue_mode` | `claim-at-open` \| `stamp-at-merge` | `claim-at-open` | See [stamp-at-merge](#stamp-at-merge). `claim-at-open` is today's queue (`gstack-next-version` claims a slot when the PR opens). |
| `release_tool` | command or `null` | `null` | The repo's own release command (`bun run release:restamp`). Runs in an isolated staging worktree under the [release-tool contract](#release-tool-contract). |
| `release_outputs` | globs | `[]` | Paths the release tool may write beyond the version files, lockfiles, CHANGELOG and `stamp_paths`. Anything else is `RESTAMP_OUTPUT_OUTSIDE_ALLOWED`. |
+1 -4
View File
@@ -101,10 +101,7 @@ export function classifyTrainHead(o: { repoRoot: string; baseRef: string; head:
if (!mergeBase) return { kind: 'invalid', reason: `no merge base between ${base} and ${o.head}` };
const npmVersion = (rev: string) => { try { return JSON.parse(show(rev, 'package.json') ?? '{}').version ?? null; } catch { return null; } };
if (policy.bump === 'never') {
return show(mergeBase, 'VERSION') === show(o.head, 'VERSION') && npmVersion(mergeBase) === npmVersion(o.head) ? { kind: 'unstamped', entry: 'kept' }
: { kind: 'invalid', reason: `the base's ship policy says bump never: leave VERSION and package.json's version at the merge base's values` };
}
if (policy.bump === 'never') return show(mergeBase, 'VERSION') === show(o.head, 'VERSION') && npmVersion(mergeBase) === npmVersion(o.head) ? { kind: 'unstamped', entry: 'kept' } : { kind: 'off' };
const changed = (git(o.repoRoot, ['diff', '--name-only', mergeBase, o.head]) ?? '').split('\n').filter(Boolean);
const releaseOutputs: string[] = Array.isArray(policy.release_outputs) ? policy.release_outputs : [];
// touched: what marks the head as stamped (the labels only feed the length check below)
+6 -4
View File
@@ -26,9 +26,9 @@ function run(r: FixtureRepo, head: string, extra: Record<string, string> = {}, c
}
describe('version gate: bump never', () => {
// Value: protects=under bump never the Version Gate passes a head that keeps VERSION and fails one that moves it; fails_when=the gate rejects every kept-version PR as "VERSION not bumped", or lets a unit PR bump;
// why_new=GST-380 added bump never (outside-voice review); seam=classifyTrainHead, scripts/compare-pr-version.ts; no_claim=the claim queue
test('a kept VERSION passes and a moved VERSION fails', () => {
// Value: protects=under bump never the Version Gate passes a head that keeps VERSION, and a release head that moves it goes through the ordinary bump and collision rules; fails_when=the gate rejects every kept-version PR as "VERSION not bumped", or rejects every release PR;
// why_new=GST-380 added bump never (outside-voice review, fix-check); seam=classifyTrainHead, scripts/compare-pr-version.ts; no_claim=who may open a release PR
test('a kept VERSION passes; a moved VERSION takes the ordinary rules', () => {
const r = makeFixtureRepo({ policy: { ...BASE_POLICY, queue_mode: 'claim-at-open', release_tool: null, release_outputs: [], bump: 'never', min_gstack_version: BUMP_NEVER_KEY_VERSION }, prefix: 'version-gate-never-' });
repos.push(r);
r.git('checkout', '-q', '-b', 'k');
@@ -37,7 +37,9 @@ describe('version gate: bump never', () => {
expect([kept.code, kept.out.includes('as the base\'s ship policy (bump never) requires')]).toEqual([0, true]);
write(r, 'VERSION', '1.2.4.0\n');
const moved = run(r, commit(r, 'bump'));
expect([moved.code, moved.out.includes('the base\'s ship policy says bump never')]).toEqual([1, true]);
expect([moved.code, moved.out.includes('bump never')]).toEqual([0, false]);
const collides = run(r, r.git('rev-parse', 'HEAD').trim(), {}, [{ pr: 7, branch: 'other', version: '1.2.4.0' }]);
expect([collides.code, collides.out.includes('VERSION collision')]).toEqual([1, true]);
});
});