Files
PaperClipAI/scripts/build-exe-dev-image.ts
T
DottaandPaperclip 056703424a Add experimental durable exe.dev agent environments
Keep one VM per environment with independent lease cgroups, persistent workspaces, immutable runner images, and an explicit experimental gate. Add live compatibility, recovery, browser-preview, and stress campaigns.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-17 09:47:20 -05:00

31 lines
2.5 KiB
TypeScript

import { execFileSync, spawnSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { computeDaytonaImageContentId, DAYTONA_IMAGE_INPUT_PATHS, extractDaytonaDockerfileFrontendDigest } from "../tests/runner-e2e/daytona-image-content.js";
const root = path.resolve(import.meta.dirname, "..");
const args = process.argv.slice(2);
const option = (name: string) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
const dockerfile = await readFile(path.join(root, "docker/exe-dev-runner/Dockerfile"), "utf8");
const revision = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
const dirty = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8" }).trim().length > 0;
if (dirty && process.env.PAPERCLIP_IMAGE_ALLOW_DIRTY !== "1") throw new Error("Commit the image inputs before building, or explicitly set PAPERCLIP_IMAGE_ALLOW_DIRTY=1 for a development image");
// COPY packages includes the workspace graph. Hash every source-owned package
// input so changes outside the runner can never reuse an old content tag.
const packageInputs = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "packages"], { cwd: root, encoding: "utf8" }).split("\0").filter(Boolean);
const contentId = await computeDaytonaImageContentId({
repositoryRoot: root,
inputPaths: [...new Set([...DAYTONA_IMAGE_INPUT_PATHS.filter((p) => p !== "docker/daytona-runner/Dockerfile" && !p.startsWith("packages/")), ...packageInputs, "server/package.json", "ui/package.json", "cli/package.json", "docker/exe-dev-runner", "scripts/build-exe-dev-image.ts"])].sort(),
baseImages: [...dockerfile.matchAll(/^FROM (\S+@sha256:[a-f0-9]{64})/gm)].map((match) => match[1]),
frontendDigest: extractDaytonaDockerfileFrontendDigest(dockerfile),
});
if (args.includes("--content-id")) { console.log(contentId); process.exit(0); }
const tag = option("--tag") ?? `ghcr.io/paperclipai/paperclip-exe-runner:experimental-${contentId}`;
const build = spawnSync("docker", ["buildx", "build", "--platform", "linux/amd64", "--file", "docker/exe-dev-runner/Dockerfile",
"--build-arg", `PAPERCLIP_RUNNER_SOURCE_REVISION=${revision}`,
"--build-arg", `PAPERCLIP_RUNNER_CONTENT_ID=${contentId}`,
"--tag", tag, ...(args.includes("--push") ? ["--push"] : ["--load"]),
...(option("--metadata-file") ? ["--metadata-file", option("--metadata-file")!] : []), "."], { cwd: root, stdio: "inherit" });
if (build.error) throw build.error;
process.exitCode = build.status ?? 1;