Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
30 changes: 24 additions & 6 deletions src/code-contrib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <repo> --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 =
Expand Down
31 changes: 31 additions & 0 deletions test/code-contrib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
});
});
Loading