fix(ship-preflight): read the default branch from the remote's HEAD, not a local origin/HEAD

A Capy checkout points refs/remotes/origin/HEAD at the PR branch, so the preflight refused every lane as 'on the base branch'. ls-remote --symref is authoritative; the local ref stays the offline fallback. Failing-first test in test/ship-preflight.test.ts.

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-30 02:07:25 +00:00
1 parent 150e44b082
commit 628278ebe4
2 files changed
+13 -1

No files matched your search

+2 -1
View File
@@ -22,7 +22,8 @@ const DIST = /(^|\/)(cmd\/[^/]+\/main\.go|bin\/[^/]+|Cargo\.toml|setup\.py|packa
function integrate(cwd: string, base: string, r: Report): boolean {
const branch = git(cwd, ['branch', '--show-current']).stdout.trim();
const head = git(cwd, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']).stdout.trim().replace(/^origin\//, '');
const remote = /^ref: refs\/heads\/(\S+)\s+HEAD$/m.exec(git(cwd, ['ls-remote', '--symref', 'origin', 'HEAD'], 60_000).stdout)?.[1];
const head = remote ?? git(cwd, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']).stdout.trim().replace(/^origin\//, '');
r.add(`BRANCH: ${branch || '(detached)'} base=${base}`);
if (!branch || branch === base || branch === head) { r.stop(`on the base branch (${branch || 'detached HEAD'}): ship from a feature branch`); return false; }
const status = git(cwd, ['status', '--porcelain']).stdout.split('\n').filter(Boolean);
+11
View File
@@ -45,6 +45,17 @@ describe('gstack-ship-preflight', () => {
expect(fetch.stdout).not.toContain('MERGE:');
});
// Value: protects=the default-branch abort reads the remote's HEAD, not a local origin/HEAD a clone pointed at the feature branch; fails_when=a checkout whose local origin/HEAD names the feature branch is refused as "on the base branch";
// why_new=GSTA-167 dogfood on #3633: a Capy checkout's origin/HEAD is the PR branch; seam=git ls-remote --symref; no_claim=offline fallback
test('a local origin/HEAD naming the feature branch is not the default branch', () => {
const f = fx();
f.git('update-ref', 'refs/remotes/origin/feat', 'HEAD');
f.git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/feat');
const r = f.run(PREFLIGHT, ['--base', 'main']);
expect(r.stdout).not.toContain('STOP: on the base branch');
expect(r.status).toBe(0);
});
// Value: protects=Step 3 merges the base and a conflict stops with the files named, leaving the merge for the agent to resolve; fails_when=the bin resolves a conflict itself, aborts the merge, or continues to tests;
// why_new=moved from the Step 3 prose; seam=git merge; no_claim=which side is right
test('a merged base is reported; a conflict stops with the conflicted files and the merge left in place', () => {