diff --git a/package-lock.json b/package-lock.json index e714474..bc84337 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "givework", - "version": "0.4.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "givework", - "version": "0.4.0", + "version": "0.5.1", "license": "Apache-2.0", "dependencies": { "@hono/node-server": "^1.13.7", diff --git a/package.json b/package.json index 34e2d13..41473bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "givework", - "version": "0.5.0", + "version": "0.5.1", "type": "module", "description": "Volunteer your AI agent's spare capacity to open mathematics", "license": "Apache-2.0", diff --git a/src/code-contrib.ts b/src/code-contrib.ts index b090e33..e3fb3ec 100644 --- a/src/code-contrib.ts +++ b/src/code-contrib.ts @@ -112,15 +112,33 @@ export async function publishCodeContribution( await run('git', ['commit', '-s', '-m', message], dir); // Direct push if this volunteer has rights on the contrib repo; fall back - // to the fork flow (gh handles creating/reusing the fork) otherwise. + // to the fork flow otherwise. let head = branch; try { await run('git', ['push', '-u', 'origin', branch], dir); - } catch { - await run('gh', ['repo', 'fork', repo, '--remote', '--remote-name', 'contribfork'], dir); - await run('git', ['push', '-u', 'contribfork', branch], dir); - const login = (await run('gh', ['api', 'user', '--jq', '.login'], dir)).trim(); - head = `${login}:${branch}`; + } catch (pushErr) { + try { + // `gh repo fork --remote` is rejected by gh ("the --remote flag + // is unsupported when a repository argument is provided"), so fork + // without touching remotes and wire the fork up with plain git. + await run('gh', ['repo', 'fork', repo, '--clone=false'], dir); + const login = (await run('gh', ['api', 'user', '--jq', '.login'], dir)).trim(); + const name = repo.split('/')[1] ?? repo; + await run( + 'git', + ['remote', 'add', 'contribfork', `https://github.com/${login}/${name}.git`], + dir, + ); + await run('git', ['push', '-u', 'contribfork', branch], dir); + head = `${login}:${branch}`; + } catch (forkErr) { + // Both causes, or the runner only ever sees the fork error and the + // reason the direct push was refused stays invisible. + throw new Error( + `direct push refused (${(pushErr as Error).message}); ` + + `fork fallback failed (${(forkErr as Error).message})`, + ); + } } const body = diff --git a/test/code-contrib.test.ts b/test/code-contrib.test.ts index c90b3e5..d8be992 100644 --- a/test/code-contrib.test.ts +++ b/test/code-contrib.test.ts @@ -71,8 +71,10 @@ describe('publishCodeContribution', () => { it('falls back to the fork flow when direct push is refused', async () => { const calls: string[] = []; + const argv: string[][] = []; const run = async (cmd: string, args: string[]) => { calls.push(`${cmd} ${args.slice(0, 2).join(' ')}`); + argv.push([cmd, ...args]); if (cmd === 'git' && args[0] === 'push' && args.includes('origin')) throw new Error('permission denied'); if (cmd === 'gh' && args[0] === 'api') return 'volunteer-login\n'; @@ -83,5 +85,34 @@ describe('publishCodeContribution', () => { expect(pub.branch.startsWith('volunteer-login:contrib/ffff0000-')).toBe(true); expect(calls).toContain('gh repo fork'); expect(pub.pr_url).toBe('https://github.com/o/r/pull/9'); + + // gh refuses --remote/--remote-name alongside a repository argument. + const fork = argv.find((a) => a[0] === 'gh' && a[1] === 'repo' && a[2] === 'fork'); + expect(fork).toBeDefined(); + expect(fork).toContain('o/r'); + expect(fork?.some((a) => a.startsWith('--remote'))).toBe(false); + // The fork remote is wired up with plain git instead. + const remote = argv.find((a) => a[0] === 'git' && a[1] === 'remote'); + expect(remote).toEqual([ + 'git', + 'remote', + 'add', + 'contribfork', + 'https://github.com/volunteer-login/r.git', + ]); + expect(argv.some((a) => a[0] === 'git' && a[1] === 'push' && a.includes('contribfork'))).toBe( + true, + ); + }); + + it('reports both causes when the fork fallback also fails', async () => { + const run = async (cmd: string, args: string[]) => { + if (cmd === 'git' && args[0] === 'push') throw new Error('could not read Username'); + if (cmd === 'gh' && args[0] === 'repo') throw new Error('gh repo exited 1: HTTP 404'); + return ''; + }; + await expect( + publishCodeContribution(cc, { taskId: 'ffff0000-x', repo: 'o/r', run }), + ).rejects.toThrow(/direct push refused .*could not read Username.*fork fallback failed .*404/s); }); });