fix: resolve gitbundle source from repo ref

This commit is contained in:
Codex
2026-06-08 15:04:08 +08:00
parent e753b853ce
commit 32318ea881
11 changed files with 161 additions and 66 deletions
+65 -18
View File
@@ -43,14 +43,24 @@ interface MaterializedSkillRef {
interface GitCheckout {
repoUrl: string;
commitId: string;
requestedCommitId?: string;
requestedRef?: string;
checkoutPath: string;
treeId: string;
}
interface GitBundleSource {
repoUrl: string;
commitId?: string;
ref?: string;
}
interface MaterializedGitBundle {
name: string | null;
repoUrl: string;
commitId: string;
requestedCommitId: string | null;
requestedRef: string | null;
subpath: string;
targetPath: string;
sourceKind: "file" | "directory";
@@ -67,18 +77,19 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
await rm(assemblyRoot, { recursive: true, force: true });
await mkdir(checkoutRoot, { recursive: true });
await mkdir(workspacePath, { recursive: true });
const defaultSource = defaultGitBundleSource(resourceBundleRef, env);
const checkoutCache = new Map<string, Promise<GitCheckout>>();
const checkoutFor = (repoUrl: string, commitId: string) => {
const key = stableHash({ repoUrl, commitId });
const checkoutFor = (source: GitBundleSource) => {
const key = stableHash(gitSourceIdentity(source));
let checkout = checkoutCache.get(key);
if (!checkout) {
checkout = checkoutGitCommit(checkoutRoot, repoUrl, commitId);
checkout = checkoutGitSource(checkoutRoot, source);
checkoutCache.set(key, checkout);
}
return checkout;
};
const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, checkoutFor);
const defaultCheckout = await checkoutFor(resourceBundleRef.repoUrl, resourceBundleRef.commitId);
const defaultCheckout = await checkoutFor(defaultSource);
const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor);
const tools = await prepareGitBundleTools(workspacePath);
const skills = await discoverGitBundleSkills(workspacePath);
const prompts = await materializePromptRefs(defaultCheckout.checkoutPath, resourceBundleRef.promptRefs ?? []);
@@ -92,7 +103,9 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
phase: "resource-bundle-materialized",
kind: "gitbundle",
repoUrl: resourceBundleRef.repoUrl,
commitId: resourceBundleRef.commitId,
commitId: defaultCheckout.commitId,
requestedCommitId: resourceBundleRef.commitId ?? null,
requestedRef: defaultCheckout.requestedRef ?? null,
treeId: defaultCheckout.treeId,
checkoutPath: pathSummary(defaultCheckout.checkoutPath),
workspacePath: pathSummary(workspacePath),
@@ -110,26 +123,56 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
};
}
async function checkoutGitCommit(checkoutRoot: string, repoUrl: string, commitId: string): Promise<GitCheckout> {
const checkoutPath = path.join(checkoutRoot, stableHash({ repoUrl, commitId }).slice(0, 16));
function defaultGitBundleSource(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv): GitBundleSource {
const ref = optionalNonEmpty(resourceBundleRef.ref) ?? optionalNonEmpty(env.AGENTRUN_RESOURCE_BUNDLE_REF) ?? optionalNonEmpty(env.AGENTRUN_WORKSPACE_REF) ?? optionalNonEmpty(env.AGENTRUN_WORKSPACE_BRANCH);
if (ref) return { repoUrl: resourceBundleRef.repoUrl, ref };
const commitId = optionalNonEmpty(resourceBundleRef.commitId);
if (commitId) return { repoUrl: resourceBundleRef.repoUrl, commitId };
return { repoUrl: resourceBundleRef.repoUrl, ref: "HEAD" };
}
function bundleGitSource(bundle: ResourceBundleRef["bundles"][number], resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource): GitBundleSource {
const repoUrl = bundle.repoUrl ?? resourceBundleRef.repoUrl;
const ref = optionalNonEmpty(bundle.ref);
if (ref) return { repoUrl, ref };
const commitId = optionalNonEmpty(bundle.commitId);
if (commitId) return { repoUrl, commitId };
if (repoUrl === defaultSource.repoUrl) return defaultSource;
if (defaultSource.ref) return { repoUrl, ref: defaultSource.ref };
if (defaultSource.commitId) return { repoUrl, commitId: defaultSource.commitId };
return { repoUrl, ref: "HEAD" };
}
async function checkoutGitSource(checkoutRoot: string, source: GitBundleSource): Promise<GitCheckout> {
const checkoutPath = path.join(checkoutRoot, stableHash(gitSourceIdentity(source)).slice(0, 16));
await mkdir(checkoutPath, { recursive: true });
await git(["init"], checkoutPath);
await git(["remote", "remove", "origin"], checkoutPath, { allowFailure: true });
await git(["remote", "add", "origin", repoUrl], checkoutPath);
await git(["fetch", "--depth", "1", "origin", commitId], checkoutPath);
await git(["checkout", "--detach", commitId], checkoutPath);
await git(["remote", "add", "origin", source.repoUrl], checkoutPath);
if (source.ref) {
await git(["fetch", "--depth", "1", "origin", source.ref], checkoutPath);
await git(["checkout", "--detach", "FETCH_HEAD"], checkoutPath);
} else if (source.commitId) {
await git(["fetch", "--depth", "1", "origin", source.commitId], checkoutPath);
await git(["checkout", "--detach", source.commitId], checkoutPath);
} else {
throw new AgentRunError("schema-invalid", "gitbundle source must include repo ref or commit", { httpStatus: 400 });
}
const actualCommit = (await git(["rev-parse", "HEAD"], checkoutPath)).stdout.trim();
if (actualCommit !== commitId) throw new AgentRunError("infra-failed", "gitbundle checkout did not land on requested commit", { httpStatus: 500, details: { expectedCommit: commitId, actualCommit } });
if (source.commitId && actualCommit !== source.commitId) throw new AgentRunError("infra-failed", "gitbundle checkout did not land on requested commit", { httpStatus: 500, details: { expectedCommit: source.commitId, actualCommit } });
const treeId = (await git(["rev-parse", "HEAD^{tree}"], checkoutPath)).stdout.trim();
return { repoUrl, commitId, checkoutPath, treeId };
return { repoUrl: source.repoUrl, commitId: actualCommit, ...(source.commitId ? { requestedCommitId: source.commitId } : {}), ...(source.ref ? { requestedRef: source.ref } : {}), checkoutPath, treeId };
}
async function materializeGitBundles(workspacePath: string, resourceBundleRef: ResourceBundleRef, checkoutFor: (repoUrl: string, commitId: string) => Promise<GitCheckout>): Promise<MaterializedGitBundle[]> {
function gitSourceIdentity(source: GitBundleSource): JsonRecord {
return { repoUrl: source.repoUrl, commitId: source.commitId ?? null, ref: source.ref ?? null };
}
async function materializeGitBundles(workspacePath: string, resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource, defaultCheckout: GitCheckout, checkoutFor: (source: GitBundleSource) => Promise<GitCheckout>): Promise<MaterializedGitBundle[]> {
const items: MaterializedGitBundle[] = [];
for (const [index, bundle] of resourceBundleRef.bundles.entries()) {
const repoUrl = bundle.repoUrl ?? resourceBundleRef.repoUrl;
const commitId = bundle.commitId ?? resourceBundleRef.commitId;
const checkout = await checkoutFor(repoUrl, commitId);
const gitSource = bundleGitSource(bundle, resourceBundleRef, defaultSource);
const checkout = gitSource === defaultSource ? defaultCheckout : await checkoutFor(gitSource);
const source = resolveBundlePath(checkout.checkoutPath, bundle.subpath, `bundles[${index}].subpath`);
const target = resolveWorkspaceTargetPath(workspacePath, bundle.targetPath, `bundles[${index}].target_path`);
let sourceStat;
@@ -141,11 +184,15 @@ async function materializeGitBundles(workspacePath: string, resourceBundleRef: R
await mkdir(path.dirname(target), { recursive: true });
await rm(target, { recursive: true, force: true });
await cp(source, target, { recursive: true, force: true, dereference: false });
items.push({ name: bundle.name ?? null, repoUrl, commitId, subpath: bundle.subpath, targetPath: bundle.targetPath, sourceKind: sourceStat.isDirectory() ? "directory" : "file", sourceBytes: sourceStat.isFile() ? sourceStat.size : null });
items.push({ name: bundle.name ?? null, repoUrl: checkout.repoUrl, commitId: checkout.commitId, requestedCommitId: bundle.commitId ?? resourceBundleRef.commitId ?? null, requestedRef: checkout.requestedRef ?? null, subpath: bundle.subpath, targetPath: bundle.targetPath, sourceKind: sourceStat.isDirectory() ? "directory" : "file", sourceBytes: sourceStat.isFile() ? sourceStat.size : null });
}
return items;
}
function optionalNonEmpty(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
async function prepareGitBundleTools(workspacePath: string): Promise<{ binPath?: string; event: JsonRecord }> {
const binPath = path.join(workspacePath, "tools");
let entries;