feat: 补齐 HWLAB 手动调度能力
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { AgentRunError } from "../common/errors.js";
|
||||
import { redactText } from "../common/redaction.js";
|
||||
import type { JsonRecord, ResourceBundleRef } from "../common/types.js";
|
||||
import { stableHash } from "../common/validation.js";
|
||||
|
||||
export interface MaterializedResourceBundle {
|
||||
workspacePath: string;
|
||||
event: JsonRecord;
|
||||
}
|
||||
|
||||
export async function materializeResourceBundle(resourceBundleRef: ResourceBundleRef | null | undefined, env: NodeJS.ProcessEnv = process.env): Promise<MaterializedResourceBundle | null> {
|
||||
if (!resourceBundleRef) return null;
|
||||
const workspaceRoot = path.resolve(env.AGENTRUN_WORKSPACE_ROOT ?? "/home/agentrun/workspaces");
|
||||
const checkoutPath = path.join(workspaceRoot, stableHash({ repoUrl: resourceBundleRef.repoUrl, commitId: resourceBundleRef.commitId }).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", resourceBundleRef.repoUrl], checkoutPath);
|
||||
if (resourceBundleRef.sparsePaths && resourceBundleRef.sparsePaths.length > 0) {
|
||||
await git(["config", "core.sparseCheckout", "true"], checkoutPath);
|
||||
await mkdir(path.join(checkoutPath, ".git", "info"), { recursive: true });
|
||||
await writeFile(path.join(checkoutPath, ".git", "info", "sparse-checkout"), `${resourceBundleRef.sparsePaths.join("\n")}\n`, "utf8");
|
||||
}
|
||||
await git(["fetch", "--depth", "1", "origin", resourceBundleRef.commitId], checkoutPath);
|
||||
await git(["checkout", "--detach", resourceBundleRef.commitId], checkoutPath);
|
||||
const actualCommit = (await git(["rev-parse", "HEAD"], checkoutPath)).stdout.trim();
|
||||
if (actualCommit !== resourceBundleRef.commitId) throw new AgentRunError("infra-failed", "resource bundle checkout did not land on requested commit", { httpStatus: 500, details: { expectedCommit: resourceBundleRef.commitId, actualCommit } });
|
||||
const treeId = (await git(["rev-parse", "HEAD^{tree}"], checkoutPath)).stdout.trim();
|
||||
const workspacePath = resolveWorkspacePath(checkoutPath, resourceBundleRef.subdir);
|
||||
return {
|
||||
workspacePath,
|
||||
event: {
|
||||
phase: "resource-bundle-materialized",
|
||||
kind: "git",
|
||||
repoUrl: resourceBundleRef.repoUrl,
|
||||
commitId: resourceBundleRef.commitId,
|
||||
treeId,
|
||||
checkoutPath: pathSummary(checkoutPath),
|
||||
workspacePath: pathSummary(workspacePath),
|
||||
subdir: resourceBundleRef.subdir ?? null,
|
||||
sparsePathCount: resourceBundleRef.sparsePaths?.length ?? 0,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function git(args: string[], cwd: string, options: { allowFailure?: boolean } = {}): Promise<{ stdout: string; stderr: string }> {
|
||||
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => { stdout += String(chunk); });
|
||||
child.stderr.on("data", (chunk) => { stderr += String(chunk); });
|
||||
const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
|
||||
child.on("error", reject);
|
||||
child.on("close", (code, signal) => resolve({ code, signal }));
|
||||
}).catch((error: unknown) => {
|
||||
throw new AgentRunError("infra-failed", `failed to start git: ${error instanceof Error ? error.message : String(error)}`, { httpStatus: 503 });
|
||||
});
|
||||
if (result.code !== 0 && !options.allowFailure) {
|
||||
throw new AgentRunError("infra-failed", `git ${args[0] ?? "command"} failed with code ${result.code}`, { httpStatus: 502, details: { stderr: redactText(stderr.slice(-4000)), stdout: redactText(stdout.slice(-1000)), signal: result.signal } });
|
||||
}
|
||||
return { stdout, stderr };
|
||||
}
|
||||
|
||||
function resolveWorkspacePath(checkoutPath: string, subdir: string | undefined): string {
|
||||
if (!subdir) return checkoutPath;
|
||||
const resolved = path.resolve(checkoutPath, subdir);
|
||||
const root = path.resolve(checkoutPath);
|
||||
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) throw new AgentRunError("schema-invalid", "resource bundle subdir escaped checkout", { httpStatus: 400 });
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function pathSummary(value: string): JsonRecord {
|
||||
const parts = value.split(/[\\/]+/u).filter(Boolean);
|
||||
return { absolute: path.isAbsolute(value), basename: parts.at(-1) ?? null, depth: parts.length, fingerprint: stableHash(value).slice(0, 16), valuePrinted: false };
|
||||
}
|
||||
Reference in New Issue
Block a user