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
29 changes: 26 additions & 3 deletions packages/core/src/evaluation/results-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,27 @@ export function normalizeResultsConfig(
};
}

// GitHub `owner/repo` shorthand: exactly two non-empty path segments and no
// scheme, host, or extra slashes. A bare local path like `.` does not match
// (no `/`), so it is never expanded into a clone URL.
const GITHUB_OWNER_REPO_SHORTHAND = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;

function isExplicitRemoteUrl(value: string): boolean {
return value.includes('://') || value.startsWith('git@');
}

export function resolveResultsRepoUrl(repo: string): string {
if (repo.includes('://') || repo.startsWith('git@')) {
return repo;
const trimmed = repo.trim();
if (isExplicitRemoteUrl(trimmed)) {
return trimmed;
}
return `https://github.com/${repo}.git`;
// Only genuine GitHub `owner/repo` shorthand is expanded to a clone URL.
// Local paths (including `.`) and other non-URL values are returned unchanged
// so we never synthesize a bogus URL such as `https://github.com/..git`.
if (GITHUB_OWNER_REPO_SHORTHAND.test(trimmed)) {
return `https://github.com/${trimmed}.git`;
}
return trimmed;
}

export function getResultsRepoLocalPaths(repo: string): ResultsRepoLocalPaths {
Expand Down Expand Up @@ -606,6 +622,13 @@ async function ensureResultsRepoRemote(
}

const remoteUrl = resolveResultsRepoUrl(config.repo_url);
// Same-repo results push to the project's existing remote (typically
// `origin`). Never rewrite that remote to a value that is not a real Git
// remote URL (e.g. a local results path or `.`), which would otherwise
// clobber a correct origin with a synthesized URL.
if (!isExplicitRemoteUrl(remoteUrl)) {
return;
}
const { stdout } = await runGit(['remote', 'get-url', config.remote], {
cwd: repoDir,
check: false,
Expand Down
27 changes: 27 additions & 0 deletions packages/core/test/evaluation/results-repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
normalizeResultsConfig,
pushWipCheckpoint,
readGitResultArtifact,
resolveResultsRepoUrl,
setupWipWorktree,
syncResultsRepo,
syncResultsRepoForProject,
Expand Down Expand Up @@ -532,6 +533,32 @@ describe('listGitRuns', () => {
});
});

describe('resolveResultsRepoUrl', () => {
it('expands genuine GitHub owner/repo shorthand to a clone URL', () => {
expect(resolveResultsRepoUrl('EntityProcess/agentv')).toBe(
'https://github.com/EntityProcess/agentv.git',
);
});

it('returns explicit remote URLs unchanged', () => {
expect(resolveResultsRepoUrl('https://github.com/example/source.git')).toBe(
'https://github.com/example/source.git',
);
expect(resolveResultsRepoUrl('git@github.com:example/source.git')).toBe(
'git@github.com:example/source.git',
);
expect(resolveResultsRepoUrl('file:///tmp/results.git')).toBe('file:///tmp/results.git');
});

it('never synthesizes a GitHub URL from a local path like "."', () => {
// Regression: same-repo results (`repo_path: .`) previously produced the
// malformed remote `https://github.com/..git` and overrode the project's
// correct origin. A local path must be returned unchanged.
expect(resolveResultsRepoUrl('.')).toBe('.');
expect(resolveResultsRepoUrl('/data/repos/WTG.AI.Prompts')).toBe('/data/repos/WTG.AI.Prompts');
});
});

describe('results repo write path', () => {
let rootDir: string;

Expand Down
Loading