diff --git a/lib/ship-preflight.ts b/lib/ship-preflight.ts index 7891b2944..42bfd847a 100644 --- a/lib/ship-preflight.ts +++ b/lib/ship-preflight.ts @@ -7,7 +7,7 @@ */ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { bin, firstLine, flagReader, git, laneLine, lanes, Report, runLanes, short, sh } from './ship-bin-common'; +import { bin, firstLine, flagReader, git, laneLine, lanes, Report, runLanes, shipPolicy, short, sh } from './ship-bin-common'; export const PREFLIGHT_HELP = `gstack-ship-preflight — /ship Steps 1-3 and Step 5's pregate and lanes in one call (ship/SKILL.md) @@ -29,7 +29,7 @@ function integrate(cwd: string, base: string, r: Report): boolean { r.add(`STATUS: modified=${status.filter(l => !l.startsWith('??')).length} untracked=${status.filter(l => l.startsWith('??')).length} (uncommitted changes ship with this run)`); const fetch = git(cwd, ['fetch', 'origin', base], 300_000); if (fetch.status !== 0) { r.stop(`git fetch origin ${base} failed: ${firstLine(fetch.stderr)}; restore access first`); return false; } - const stat = git(cwd, ['diff', `origin/${base}`, '--shortstat']).stdout.trim(); + const stat = git(cwd, ['diff', forkPoint(cwd, base), '--shortstat']).stdout.trim(); const lines = [...stat.matchAll(/(\d+) (insertion|deletion)/g)].reduce((n, m) => n + Number(m[1]), 0); const commits = git(cwd, ['rev-list', '--count', `origin/${base}..HEAD`]).stdout.trim(); r.add(`DIFF: ${stat || 'no changes'}; commits=${commits || 0}${lines > 200 ? '; over 200 lines: recommend /plan-eng-review or /autoplan' : ''}`); @@ -52,19 +52,25 @@ function integrate(cwd: string, base: string, r: Report): boolean { return merge(cwd, base, r); } +const forkPoint = (cwd: string, base: string) => git(cwd, ['merge-base', `origin/${base}`, 'HEAD']).stdout.trim() || `origin/${base}`; + function merge(cwd: string, base: string, r: Report): boolean { - const before = git(cwd, ['rev-parse', 'HEAD']).stdout.trim(); - const m = git(cwd, ['merge', `origin/${base}`, '--no-edit'], 300_000); - if (m.status !== 0) { - const conflicts = git(cwd, ['diff', '--name-only', '--diff-filter=U']).stdout.split('\n').filter(Boolean); - r.stop(conflicts.length - ? `merge conflicts in ${short(conflicts, 8)}: resolve simple ones (VERSION, schema.rb, CHANGELOG ordering) yourself; for complex or ambiguous ones ask (Step 3), then commit the merge and rerun` - : `git merge origin/${base} failed: ${firstLine(m.stderr || m.stdout)}`); - return false; + const policy = shipPolicy(cwd, base); + if (policy?.queue_mode === 'stamp-at-merge') r.add('MERGE: skipped (queue_mode stamp-at-merge: the slot stamp merges the base; docs/ship-policy.md#ship-before-ready)'); + else { + const before = git(cwd, ['rev-parse', 'HEAD']).stdout.trim(); + const m = git(cwd, ['merge', `origin/${base}`, '--no-edit'], 300_000); + if (m.status !== 0) { + const conflicts = git(cwd, ['diff', '--name-only', '--diff-filter=U']).stdout.split('\n').filter(Boolean); + r.stop(conflicts.length + ? `merge conflicts in ${short(conflicts, 8)}: resolve simple ones (VERSION, schema.rb, CHANGELOG ordering) yourself; for complex or ambiguous ones ask (Step 3), then commit the merge and rerun` + : `git merge origin/${base} failed: ${firstLine(m.stderr || m.stdout)}`); + return false; + } + const after = git(cwd, ['rev-parse', 'HEAD']).stdout.trim(); + r.add(after === before ? 'MERGE: up to date' : `MERGE: merged origin/${base} at ${after.slice(0, 12)}`); } - const after = git(cwd, ['rev-parse', 'HEAD']).stdout.trim(); - r.add(after === before ? 'MERGE: up to date' : `MERGE: merged origin/${base} at ${after.slice(0, 12)}`); - const added = git(cwd, ['diff', `origin/${base}`, '--diff-filter=A', '--name-only']).stdout.split('\n'); + const added = git(cwd, ['diff', forkPoint(cwd, base), '--diff-filter=A', '--name-only']).stdout.split('\n'); const untracked = git(cwd, ['ls-files', '--others', '--exclude-standard']).stdout.split('\n'); const candidates = [...new Set([...added, ...untracked].filter(p => p && DIST.test(p)))]; if (!candidates.length) r.add('DISTRIBUTION: no new artifact paths'); @@ -74,9 +80,7 @@ function merge(cwd: string, base: string, r: Report): boolean { r.add(`DISTRIBUTION: candidates=${short(candidates, 5)} release_workflow=${[...wf, ...(gitlab ? ['GITLAB_CI_RELEASE'] : [])].join(',') || 'none'}`) .next('Step 2: read each candidate (and manifests for new binaries or exports); a new distributable without a pipeline needs Step 2\'s question'); } - const policy = bin(cwd, 'gstack-ship-policy', ['show-effective', '--base', base], 60_000).stdout; - const pick = (k: string) => (new RegExp(`^${k}=(\\S+)`, 'm').exec(policy)?.[1] ?? '?').replace(/^"|"$/g, ''); - r.add(`POLICY: ${['bump', 'queue_mode', 'review_scope', 'history'].map(k => `${k}=${pick(k)}`).join(' ')} (binds Steps 12, 16 and history)`); + r.add(`POLICY: ${['bump', 'queue_mode', 'review_scope', 'history'].map(k => `${k}=${policy?.[k] ?? '?'}`).join(' ')} (binds Steps 12, 16 and history)`); const plan = bin(cwd, 'gstack-review-plan', ['--base', base, '--json'], 180_000); let runtime = 'unknown'; try { runtime = String(JSON.parse(plan.stdout).runtime_lines ?? 'unknown'); } catch { /* unknown */ } diff --git a/ship/SKILL.md b/ship/SKILL.md index b4874787c..dce7cc755 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -455,7 +455,7 @@ not web services already deployed. ## Step 3: Merge the base branch (BEFORE tests) -The preflight merges the base ref it fetched (`git merge origin/ --no-edit`) so tests and reviews cover the integrated code (`MERGE:`). +The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge). **Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight. diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index d22309414..5506c3fe2 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -185,7 +185,7 @@ not web services already deployed. ## Step 3: Merge the base branch (BEFORE tests) -The preflight merges the base ref it fetched (`git merge origin/ --no-edit`) so tests and reviews cover the integrated code (`MERGE:`). +The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge). **Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight. diff --git a/test/fixtures/golden/agy-ship-SKILL.md b/test/fixtures/golden/agy-ship-SKILL.md index 4545c8e13..f15d98e63 100644 --- a/test/fixtures/golden/agy-ship-SKILL.md +++ b/test/fixtures/golden/agy-ship-SKILL.md @@ -455,7 +455,7 @@ not web services already deployed. ## Step 3: Merge the base branch (BEFORE tests) -The preflight merges the base ref it fetched (`git merge origin/ --no-edit`) so tests and reviews cover the integrated code (`MERGE:`). +The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge). **Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight. diff --git a/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md index b4874787c..dce7cc755 100644 --- a/test/fixtures/golden/claude-ship-SKILL.md +++ b/test/fixtures/golden/claude-ship-SKILL.md @@ -455,7 +455,7 @@ not web services already deployed. ## Step 3: Merge the base branch (BEFORE tests) -The preflight merges the base ref it fetched (`git merge origin/ --no-edit`) so tests and reviews cover the integrated code (`MERGE:`). +The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge). **Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight. diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index fb51e419b..a1fbb1443 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -454,7 +454,7 @@ not web services already deployed. ## Step 3: Merge the base branch (BEFORE tests) -The preflight merges the base ref it fetched (`git merge origin/ --no-edit`) so tests and reviews cover the integrated code (`MERGE:`). +The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge). **Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight. diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index d4f32c054..f7ba84ee9 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -456,7 +456,7 @@ not web services already deployed. ## Step 3: Merge the base branch (BEFORE tests) -The preflight merges the base ref it fetched (`git merge origin/ --no-edit`) so tests and reviews cover the integrated code (`MERGE:`). +The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge). **Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight. diff --git a/test/ship-preflight.test.ts b/test/ship-preflight.test.ts index 93dda3fa4..0a71c5969 100644 --- a/test/ship-preflight.test.ts +++ b/test/ship-preflight.test.ts @@ -60,6 +60,20 @@ describe('gstack-ship-preflight', () => { expect(r.stdout).not.toContain('LANE:'); }); + // Value: protects=under queue_mode stamp-at-merge /ship never merges the base into the branch (docs/ship-policy.md#ship-before-ready; the slot stamp does), and DIFF counts only the branch's own change; fails_when=the preflight merges main anyway or DIFF counts the base's movement; + // why_new=GSTA-167 dogfood on #3633 hit the merge under stamp-at-merge; seam=gstack-ship-policy show-effective; no_claim=the slot stamp's merge + test('queue_mode stamp-at-merge: the base is not merged and DIFF is the branch alone', () => { + const f = fx({ '.gstack/ship-policy.json': '{"queue_mode":"stamp-at-merge"}\n' }); + f.advanceBase({ 'docs/new.md': '# new\n' }); + const head = f.git('rev-parse', 'HEAD').stdout.trim(); + const r = f.run(PREFLIGHT, ['--base', 'main']); + expect(r.status).toBe(0); + expect(lineOf(r.stdout, 'MERGE')).toMatch(/^MERGE: skipped \(queue_mode stamp-at-merge/); + expect(f.git('rev-parse', 'HEAD').stdout.trim()).toBe(head); + expect(lineOf(r.stdout, 'DIFF')).toMatch(/^DIFF: 1 file changed, 1 insertion\(\+\); commits=1$/); + expect(lineOf(r.stdout, 'POLICY')).toContain('queue_mode=stamp-at-merge'); + }); + // Value: protects=a failing lane and a zero-run lane both stop for Step 5's triage; fails_when=exit 0 with zero tests counts as a pass, or a failure continues to Step 6; // why_new=moved from Step 5's lane block; seam=gstack-evidence tests_ran; no_claim=the triage outcome test('a failing lane and a ZERO-RUN lane stop for Step 5 triage', () => { diff --git a/test/size-ledger/ship/gsta-167-pr34.json b/test/size-ledger/ship/gsta-167-pr34.json new file mode 100644 index 000000000..5c66f2018 --- /dev/null +++ b/test/size-ledger/ship/gsta-167-pr34.json @@ -0,0 +1,4 @@ +{ + "bytes": 1, + "why": "gstack-ship-preflight skips the base merge under queue_mode stamp-at-merge (Step 3 note)" +}