fix(core): parse non-GitHub SSH remote URLs correctly in registerRepository

When registering a repo whose remote uses a self-hosted or GitLab SSH URL
(git@host:org/repo), the previous code only normalised git@github.com: URLs.
All other SSH hosts fell through to a generic split-on-slash, leaving the
full SSH host (e.g. git@gitlab.example.com:org) as the owner component.

This caused two downstream failures:

1. The codebase name stored in the DB contained a colon
   (e.g. git@gitlab.example.com:org/repo), which made parseOwnerRepo()
   return null due to its SAFE_NAME regex. resolveProjectPaths() then fell
   back to writing artifacts inside the worktree directory instead of the
   canonical ~/.archon/workspaces/ tree, so the server could never serve
   them and the Web UI always showed "Artifact file not found".

2. The colon in the worktree path acts as a classpath separator on Unix,
   breaking Java/Maven/Gradle test compilation for any project run inside
   that worktree.

Fix: match any git@host:owner/repo SSH URL with a single regex and extract
just the owner/repo portion, discarding the host. HTTPS and other URL forms
continue to use the existing path-split fallback unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Leon Liu 2026-04-17 13:45:58 +09:00 committed by Patch Pilot
parent bed36ca4ad
commit 4e4982e4fd
2 changed files with 40 additions and 10 deletions

View file

@ -706,6 +706,32 @@ describe('registerRepository', () => {
expect(createArg.name).toBe('acme/backend');
});
test('builds owner/repo name from non-GitHub SSH remote URL without colon in name', async () => {
// GitLab / self-hosted SSH URLs: git@host:org/repo — the host must NOT appear in
// the codebase name or worktree path because colons break Java classpaths on Unix.
spyExecFileAsync.mockImplementation((cmd: string, args: string[]) => {
if (args.includes('rev-parse')) return Promise.resolve({ stdout: '.git', stderr: '' });
if (args.includes('get-url'))
return Promise.resolve({
stdout: 'git@gitlab.example.com:myorg/myproject.git',
stderr: '',
});
return Promise.resolve({ stdout: '', stderr: '' });
});
mockFindCodebaseByDefaultCwd.mockResolvedValueOnce(null);
mockCreateCodebase.mockResolvedValueOnce(
makeCodebase({ name: 'myorg/myproject' }) as ReturnType<typeof makeCodebase>
);
await registerRepository('/home/user/myproject');
const createArg = mockCreateCodebase.mock.calls[0]?.[0] as { name: string };
// Must be plain owner/repo — no SSH host, no colon
expect(createArg.name).toBe('myorg/myproject');
expect(createArg.name).not.toContain(':');
expect(createArg.name).not.toContain('@');
});
// ── Command auto-loading ───────────────────────────────────────────────
test('auto-loads markdown commands found in .archon/commands', async () => {
spyExecFileAsync.mockImplementation((cmd: string, args: string[]) => {

View file

@ -328,16 +328,20 @@ export async function registerRepository(localPath: string): Promise<RegisterRes
let ownerName = '_local';
if (remoteUrl) {
const cleaned = remoteUrl.replace(/\.git$/, '').replace(/\/+$/, '');
let workingRemote = cleaned;
if (cleaned.startsWith('git@github.com:')) {
workingRemote = cleaned.replace('git@github.com:', 'https://github.com/');
}
const parts = workingRemote.split('/');
const r = parts.pop();
const o = parts.pop();
if (o && r) {
name = `${o}/${r}`;
ownerName = o;
// Handle any SSH git URL (git@host:owner/repo) by extracting just owner/repo.
// This avoids colons in worktree paths, which break Java classpaths on Unix.
const sshMatch = /^git@[^:]+:([^/]+)\/(.+)$/.exec(cleaned);
if (sshMatch) {
name = `${sshMatch[1]}/${sshMatch[2]}`;
ownerName = sshMatch[1];
} else {
const parts = cleaned.split('/');
const r = parts.pop();
const o = parts.pop();
if (o && r) {
name = `${o}/${r}`;
ownerName = o;
}
}
}