fix(ship-receipt): record the installed gstack VERSION, not the newest policy-key version (GST-1366) (#4002)

* fix(ship-receipt): record the installed gstack VERSION, not the newest policy-key version (GST-1366)

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

* docs(changelog): fragment for GST-1366 receipt gstack_version

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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-05 00:19:30 +00:00
1 parent ef6425036e
commit 87d7f0f54e
6 files changed
+42 -9

No files matched your search

+2 -2
View File
@@ -41,7 +41,7 @@ import { branchSides, stampOnlyLine, type BranchSide } from '../lib/stamp-only';
import { runScriptSync } from '../lib/script-invocation';
import { readPregateFile, verifyPregate } from '../lib/pregate/index';
import { exceptionConsumerProblem } from '../lib/pregate/flaky-exception';
import { isUnavailable, loadPolicyFromBase, matchesAny, policyUnavailableLine, resolveBaseBranch, runningGstackVersion, stampFilesInTree, validatePolicy, type ShipPolicy } from '../lib/ship-policy';
import { installedGstackVersion, isUnavailable, loadPolicyFromBase, matchesAny, policyUnavailableLine, resolveBaseBranch, stampFilesInTree, validatePolicy, type ShipPolicy } from '../lib/ship-policy';
import { approvalForReceipt } from '../lib/approval-citations';
import {
CARRY_METADATA_GLOBS, CARRY_NAMED_PATHS, CARRY_TEST_GLOBS,
@@ -305,7 +305,7 @@ function cmdWrite(rest: string[], json: boolean): number {
policy: flag(rest, '--policy') ?? tree?.policy, queue_mode: flag(rest, '--queue-mode'), preregistration: flag(rest, '--preregistration'), history: flag(rest, '--history'),
...pregate, issues: issuesFile ? receiptIssuesFrom(issuesFile) : undefined, approval: approval.kind === 'ok' ? approval.approval : undefined,
review_loop: reviewLoop, ...(docs?.ok ? { docs: docs.docs } : {}), ...product,
gstack_version: runningGstackVersion(), review_depth: reviewDepth, review_loop_exception: loopException?.ok ? loopException.exception : undefined, review_targeted: targeted?.ok ? targeted.targeted : undefined,
gstack_version: installedGstackVersion(), review_depth: reviewDepth, review_loop_exception: loopException?.ok ? loopException.exception : undefined, review_targeted: targeted?.ok ? targeted.targeted : undefined,
base_red: pause ? receiptBaseRed(pause) : undefined,
review_exclude: receiptExclude(review.exclude),
no_stop: noStopArg(flag(rest, '--no-stop')),
@@ -0,0 +1 @@
- **A /ship receipt now names the gstack release that wrote it.** `gstack_version` in the receipt used to record the version gstack reads policies as, which jumps to the newest policy key the checkout knows. On a 1.95.5.0 checkout that carries the unreleased 1.96.0.0 keys, every receipt said 1.96.0.0, a release that never ran. The receipt now records the checkout's VERSION file. Policy reading is unchanged, so a checkout that carries a new key still reads policies written for it. (GST-1366)
+3 -2
View File
@@ -1279,8 +1279,9 @@ unreadable base policy is `POLICY_SOURCE_UNAVAILABLE`); one
(`absent/<max> over` for a path the tree lacks), exit 1 on any `over`, whose
repair is trimming the file, never raising the cap in the same PR.
**`review_depth` and `gstack_version`.** `write` records the gstack that
wrote the receipt and the review depth at the checked-out head
**`review_depth` and `gstack_version`.** `write` records the gstack release
that wrote the receipt (its `VERSION` file, never the newer policy-key version
an unstamped tree reads policies as) and the review depth at the checked-out head
([review-policy.md#review-depth](review-policy.md#review-depth)):
`{ depth, class, runtime_lines, rules, base, registry, gate_scan: { tagged, head }, reasons, risk, mode }`
(`mode` is lean, panel or platform; `declared` and `detected` appear with `write --declare-risk`, and the platform depth adds `platform_review`, see [review-policy.md](review-policy.md#review-depth)),
+1 -1
View File
@@ -118,7 +118,7 @@ export interface ShipReceipt {
carry?: { from: string; reason: string; product_patch_id: string; changed_paths: string[]; merge_commits: number; range_diff: string; covers: string[]; reruns: string[]; trees?: string[]; ancestry?: 'linear' | 'rewritten' };
/** The reviewed head's tree and its product patch-id against the base (gstack-ship-receipt write; carry rebinds both): what a carry across rewritten history compares. */
head_tree?: string; product_patch_id?: string;
/** The gstack that wrote the receipt, and the review depth the reviewed head needs, recomputed at write (lib/ship-receipt-depth.ts receiptDepthAtHead). */
/** The gstack release that wrote the receipt (its VERSION file, never the newer policy-key version it reads policies as), and the review depth the reviewed head needs, recomputed at write (lib/ship-receipt-depth.ts receiptDepthAtHead). */
gstack_version?: string;
review_depth?: { depth: 'platform' | 'light' | 'standard' | 'full'; class: 'docs' | 'tests' | 'code' | 'gate'; runtime_lines: number; rules: string; base: string; registry: string | null; gate_scan: { tagged: number | null; head: string }; reasons: string[]; risk?: string[]; mode?: 'lean' | 'panel' | 'platform'; platform_review?: { id: string; head: string; reviewer: string }; declared?: string[]; detected?: string[] };
/** The owner's acceptance of a stop after a fix-check edit under frozen scope (#3559); the PR body repeats it word for word. */
+8 -3
View File
@@ -231,14 +231,19 @@ export * from './ship-policy-keys';
const newer = (a: string, b: string): boolean => cmpVersion(parseVersion(a)!, parseVersion(b)!) > 0;
/** The release this checkout installs: its VERSION file, the version a ship receipt records (GST-1366). */
export function installedGstackVersion(): string {
try { return fs.readFileSync(path.resolve(import.meta.dir, '..', 'VERSION'), 'utf8').trim(); } catch { return '0.0.0.0'; }
}
/**
* The release this checkout reads policies as: its VERSION file, or the
* newest key it knows when that is later (an unstamped tree that carries a
* new key already reads policies written for it).
* new key already reads policies written for it). Never a recorded version:
* that is installedGstackVersion().
*/
export function runningGstackVersion(): string {
let own = '0.0.0.0';
try { own = fs.readFileSync(path.resolve(import.meta.dir, '..', 'VERSION'), 'utf8').trim(); } catch {}
const own = installedGstackVersion();
const known = [...Object.values(POLICY_KEY_INTRODUCED), BUMP_NEVER_KEY_VERSION, TRAIN_GATES_KEY_VERSION].reduce((a, b) => (newer(b, a) ? b : a));
return parseVersion(own) && !newer(known, own) ? own : known;
}
+27 -1
View File
@@ -9,10 +9,15 @@
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { EXIT } from '../lib/headless-artifacts';
import { extractReceipt, renderReceiptBlock } from '../lib/ship-receipt';
import { bindShippedReceipt, receiptDepthAtHead, receiptDepthVerdict, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
import { cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { runScriptSync } from '../lib/script-invocation';
import { BUMP_NEVER_KEY_VERSION, POLICY_KEY_INTRODUCED, TRAIN_GATES_KEY_VERSION } from '../lib/ship-policy';
import { cmpVersion, parseVersion } from '../lib/version-source';
import { cleanup, commit, makeFixtureRepo, ROOT, runBin, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
@@ -54,6 +59,27 @@ describe('write records the review depth at the checked-out head', () => {
expect(bad.err).toContain('(RECEIPT_INVALID)');
expect(fs.existsSync(other)).toBe(false);
}, 60_000);
// Value: protects=a receipt names the gstack release that wrote it, never a newer policy key its tree carries, so a
// version check against a receipt cannot be passed by an older build (GST-1366); fails_when=write records the
// key-inflated version policy reading uses, or policy reading stops seeing the newest key; seam=none
test('a gstack whose VERSION is older than its newest policy key records that VERSION; policy reading still reads at the key', () => {
const repo = makeFixtureRepo(); repos.push(repo);
commit(repo, { 'lib/util.js': lines(3) });
const installed = path.join(repo.root, 'installed-gstack');
const sourceOnly = { recursive: true, filter: (file: string) => path.basename(file) !== 'node_modules' };
for (const dir of ['bin', 'lib', 'scripts']) fs.cpSync(path.join(ROOT, dir), path.join(installed, dir), sourceOnly);
fs.writeFileSync(path.join(installed, 'VERSION'), '1.0.0.0\n');
const body = path.join(repo.root, 'body.md');
const write = runScriptSync(path.join(installed, 'bin', 'gstack-ship-receipt'), ['write', '--pr', '7', '--base', 'main', '--version', '1.0.0', '--session-kind', 'unattended', '--body', body],
{ cwd: repo.work, encoding: 'utf8', timeout: 180_000, maxBuffer: 64 * 1024 * 1024, env: { ...process.env, ...repo.env } });
expect(write.status, write.stderr).toBe(EXIT.ok);
expect(receiptIn(body).gstack_version).toBe('1.0.0.0');
const newestKey = [...Object.values(POLICY_KEY_INTRODUCED), BUMP_NEVER_KEY_VERSION, TRAIN_GATES_KEY_VERSION].reduce((a, b) => (cmpVersion(parseVersion(b)!, parseVersion(a)!) > 0 ? b : a));
const reader = spawnSync(process.execPath, [...bunIsolationArgs(), '-e', `console.log((await import(${JSON.stringify(path.join(installed, 'lib', 'ship-policy.ts'))})).runningGstackVersion())`],
{ cwd: repo.work, encoding: 'utf8', timeout: 60_000 });
expect(reader.stdout.trim(), reader.stderr).toBe(newestKey);
}, 120_000);
});
describe('the queue refuses a receipt below the depth its head needs', () => {