paperclip/packages/paperclip-runner/scripts/build-provider-pack.mjs

75 lines
5.2 KiB
JavaScript

// The trusted qualification workflow invokes this target-owned entry point.
// Never assemble from that job's CI-resolved lock, node_modules, shared dist,
// or setup-node interpreter: the image's isolated stage is the source of truth.
import { execFileSync, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync } from "node:fs";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { sha256File, verifyProviderPack } from "./provider-pack-integrity.mjs";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const defaultWorkspaceRoot = resolve(packageRoot, "../..");
export function buildProviderPack({ workspaceRoot = defaultWorkspaceRoot, outputRoot = join(packageRoot, "provider-pack"),
revision = process.env.PAPERCLIP_RUNNER_SOURCE_REVISION?.trim(), run = spawnSync } = {}) {
workspaceRoot = realpathSync(workspaceRoot);
outputRoot = resolve(outputRoot);
const missingSegments = [];
let existingParent = outputRoot;
while (!existsSync(existingParent)) { missingSegments.unshift(basename(existingParent)); existingParent = dirname(existingParent); }
outputRoot = join(realpathSync(existingParent), ...missingSegments);
const sourcePackage = join(workspaceRoot, "packages/paperclip-runner");
function contains(base, target) {
const child = relative(base, target);
return !isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`);
}
if (outputRoot === dirname(outputRoot) || contains(outputRoot, workspaceRoot)
|| (contains(workspaceRoot, outputRoot) && outputRoot !== join(sourcePackage, "provider-pack"))) {
throw new Error("Refusing unsafe provider-pack output path");
}
if (revision === undefined) {
// CI supplies an authorized revision explicitly and may refresh only its
// root lock. Local implicit HEAD must not mislabel changed image inputs.
const dirty = execFileSync("git", ["status", "--porcelain", "--untracked-files=all", "--",
".dockerignore", "docker/daytona-runner", "package.json", "pnpm-workspace.yaml", ".npmrc", "tsconfig.base.json",
"patches", "scripts/link-plugin-dev-sdk.mjs", "packages", "server/package.json", "ui/package.json", "cli/package.json",
":(exclude)packages/paperclip-runner/provider-pack", ":(glob,exclude)packages/paperclip-runner/.paperclip-provider-pack-*/**"],
{ cwd: workspaceRoot, encoding: "utf8" });
if (dirty.trim()) throw new Error("Canonical provider inputs are dirty; an implicit HEAD revision would be misleading");
revision = execFileSync("git", ["rev-parse", "HEAD"], { cwd: workspaceRoot, encoding: "utf8" }).trim();
}
if (!/^[0-9a-f]{40}$/.test(revision)) throw new Error("PAPERCLIP_RUNNER_SOURCE_REVISION must be a full Git SHA");
const dockerfile = join(workspaceRoot, "docker/daytona-runner/Dockerfile");
const expectedLock = readFileSync(dockerfile, "utf8").match(/^ARG PAPERCLIP_RUNNER_LOCK_SHA256=([a-f0-9]{64})$/m)?.[1];
if (!expectedLock) throw new Error("Canonical provider lock pin is missing");
const actualLock = sha256File(join(workspaceRoot, "docker/daytona-runner/provider-dependencies.lock.yaml"));
if (actualLock !== `sha256:${expectedLock}`) throw new Error("Canonical provider lock integrity mismatch");
mkdirSync(dirname(outputRoot), { recursive: true });
// Same filesystem as OUTPUT, so successful publication is an atomic rename.
// Docker reads its inputs before this empty directory receives exported files.
const temporaryParent = mkdtempSync(join(dirname(outputRoot), ".paperclip-provider-pack-"));
const exportRoot = join(temporaryParent, "exported"), exported = join(exportRoot, "provider-pack"), backup = join(temporaryParent, "previous");
let previousMoved = false, published = false;
try {
const build = run("docker", ["build", "--platform", "linux/amd64", "--target", "provider-pack-export",
"--file", dockerfile, "--build-arg", `PAPERCLIP_RUNNER_SOURCE_REVISION=${revision}`,
"--output", `type=local,dest=${exportRoot}`, workspaceRoot],
{ cwd: workspaceRoot, stdio: "inherit", timeout: 12 * 60 * 1000, killSignal: "SIGTERM" });
if (build.error || build.status !== 0) throw new Error("Canonical provider-pack build failed");
const manifest = verifyProviderPack(exported, { revision, lockSha256: expectedLock });
if (existsSync(outputRoot)) { renameSync(outputRoot, backup); previousMoved = true; }
try { renameSync(exported, outputRoot); published = true; }
catch (error) { if (previousMoved) { renameSync(backup, outputRoot); previousMoved = false; } throw error; }
return { outputRoot, manifest };
} finally {
// If even rollback fails, leave the prior pack in its recoverable backup.
if (!previousMoved || published) rmSync(temporaryParent, { recursive: true, force: true });
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const outputArgument = process.argv.slice(2).find((value) => value !== "--");
const result = buildProviderPack({ outputRoot: resolve(process.cwd(), outputArgument ?? join(packageRoot, "provider-pack")) });
process.stdout.write(`${result.outputRoot}\n`);
}