fix: resolve gitbundle source from repo ref
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user