From 876b064a867aed1cc480dd370a0b131d2355e2df Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 3 Nov 2026 09:14:28 +0000 Subject: [PATCH] fix(ship-release): GST-1031 re-pin route reads per-file harness pins (GST-1084) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- lib/ship-release.ts | 20 ++++++++++++-------- test/ship-release.test.ts | 23 ++++++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/ship-release.ts b/lib/ship-release.ts index 7c1941a5e..90c45d182 100644 --- a/lib/ship-release.ts +++ b/lib/ship-release.ts @@ -171,16 +171,20 @@ function commitRelease(cwd: string, release: string[], trailer: string | undefin /** * A harness pin edit that only records changed harness files as non-behavioral at the same version and patterns (GST-1031): release - * metadata, like a version stamp. A bump, a pattern change or an entry without its reason is behavior. + * metadata, like a version stamp. The pins live one file per harness file under scripts/harness-pins/ (GST-1084), beside a manifest + * holding only version and patterns. A bump, a pattern change or a pin without its reason is behavior. */ -const HARNESS_PIN = 'scripts/harness-version.json'; +const HARNESS_MANIFEST = 'scripts/harness-version.json'; +const HARNESS_PINS = 'scripts/harness-pins/'; function harnessRepin(cwd: string, from: string, to: string, p: string): boolean { - if (p !== HARNESS_PIN) return false; - const read = (tree: string): any => { const s = git(cwd, ['show', `${tree}:${p}`]); try { return s.status === 0 ? JSON.parse(s.stdout) : null; } catch { return null; } }; - const a = read(from), b = read(to); - if (!a?.files || !b?.files || typeof b.files !== 'object' || a.version !== b.version || JSON.stringify(a.patterns) !== JSON.stringify(b.patterns)) return false; - return Object.entries(b.files as Record).every(([f, e]) => JSON.stringify(a.files[f]) === JSON.stringify(e) - || (e?.since === b.version && typeof e?.nonBehavioral === 'string' && e.nonBehavioral.trim().length >= 10)); + if (p !== HARNESS_MANIFEST && !(p.startsWith(HARNESS_PINS) && p.endsWith('.json'))) return false; + const read = (tree: string, file: string): any => { const s = git(cwd, ['show', `${tree}:${file}`]); if (s.status !== 0) return undefined; try { return JSON.parse(s.stdout); } catch { return null; } }; + const a = read(from, HARNESS_MANIFEST), b = read(to, HARNESS_MANIFEST); + if (!a || !b || Object.keys(b).sort().join() !== 'patterns,version' || a.version !== b.version || JSON.stringify(a.patterns) !== JSON.stringify(b.patterns)) return false; + if (p === HARNESS_MANIFEST) return true; + const e = read(to, p); + return e === undefined || JSON.stringify(read(from, p)) === JSON.stringify(e) + || (e?.since === b.version && typeof e?.nonBehavioral === 'string' && e.nonBehavioral.trim().length >= 10); } /** Step 16 stage 2's route. With --reviewed-base, a path whose branch patch is the reviewed one counts as base (lib/ship-patch.ts). */ diff --git a/test/ship-release.test.ts b/test/ship-release.test.ts index 8b7dc72b2..49d324c79 100644 --- a/test/ship-release.test.ts +++ b/test/ship-release.test.ts @@ -309,25 +309,34 @@ describe('gstack-ship-release push: branch names', () => { }); describe('GST-1031: a non-behavioral harness re-pin after review is release metadata', () => { - // Value: protects=a scripts/harness-version.json edit after review that only records changed harness files as non-behavioral at the same + // Value: protects=a scripts/harness-pins/.json edit after review that only records changed harness files as non-behavioral at the same // version routes as metadata, so a clean round 2 is not sent back through 5-11.5, while a version bump, a pattern change or an entry // without its reason stays behavior; fails_when=the re-pin stops finish, or a bump or bare re-pin routes as metadata; // why_new=GSTA-150 GST-1027 (icm_01M6ERWWBZ3VWXZY07F2W5ZSFB); seam=route; no_claim=that the recorded reason is true - const pin = (version: number, entry: Record, patterns = ['test/helpers/x.ts']) => JSON.stringify({ version, patterns, files: { 'test/helpers/x.ts': entry } }) + '\n'; - const finish = (after: string) => { + const PIN = 'scripts/harness-pins/test/helpers/x.ts.json'; + const pin = (version: number, entry: Record, patterns = ['test/helpers/x.ts']) => ({ + 'scripts/harness-version.json': JSON.stringify({ version, patterns }) + '\n', + [PIN]: JSON.stringify(entry) + '\n', + }); + const finish = (after: Record) => { const f = fx(); - f.write({ 'scripts/harness-version.json': pin(6, { blob: 'a'.repeat(40), since: 6 }) }); f.commit('pin'); + f.write(pin(6, { blob: 'a'.repeat(40), since: 6 })); f.commit('pin'); const reviewed = wtree(f); const post = snapshot(f); - f.write({ 'scripts/harness-version.json': after }); f.commit('re-pin'); + f.write(after); f.commit('re-pin'); return f.run(RELEASE, ['finish', '--base', 'main', '--reviewed-tree', reviewed, '--docs-post', post, '--lane', LANE]).stdout; }; test('a non-behavioral re-pin is metadata; a bump, a pattern change or an entry without its reason is behavior', () => { const repin = finish(pin(6, { blob: 'b'.repeat(40), since: 6, nonBehavioral: 'comment-only edit: no verdict can change' })); expect(lineOf(repin, 'ROUTE')).toMatch(/behavior=0 docs=0 metadata=1/); expect(repin).not.toContain('STOP: behavior'); - for (const after of [pin(7, { blob: 'b'.repeat(40), since: 7 }), pin(6, { blob: 'b'.repeat(40), since: 6 }), pin(6, { blob: 'b'.repeat(40), since: 6, nonBehavioral: 'comment-only edit: no verdict can change' }, ['test/helpers/**'])]) - expect(finish(after)).toContain('STOP: behavior, tests or build inputs changed since review (scripts/harness-version.json)'); + expect(finish(pin(7, { blob: 'b'.repeat(40), since: 7 }))).toContain(`STOP: behavior, tests or build inputs changed since review (${PIN},scripts/harness-version.json)`); + expect(finish(pin(6, { blob: 'b'.repeat(40), since: 6 }))).toContain(`STOP: behavior, tests or build inputs changed since review (${PIN})`); + expect(finish(pin(6, { blob: 'b'.repeat(40), since: 6, nonBehavioral: 'comment-only edit: no verdict can change' }, ['test/helpers/**']))).toContain(`STOP: behavior, tests or build inputs changed since review (${PIN},scripts/harness-version.json)`); + }); + test('the manifest regaining a files table is behavior', () => { + const f = { 'scripts/harness-version.json': JSON.stringify({ version: 6, patterns: ['test/helpers/x.ts'], files: {} }) + '\n' }; + expect(finish(f)).toContain('STOP: behavior, tests or build inputs changed since review (scripts/harness-version.json)'); }); });