Files
PaperClipAI/scripts/build-preview-migrator.mjs
T
DottaandPaperclip 639d8875bb Build staging migrator artifacts and align native sandbox sessions with HOME
Use object-storage artifact URLs and Actions build artifacts without creating releases. Fix both native session entry points, preserve the primary workspace environment, and declare upload headers in OpenAPI. Tests cover sandbox home confinement, session dispatch, and generated headers.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-07 12:56:29 -05:00

66 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { materializePublishManifest, prepareBundledPackage } from "./prepare-bundled-package.mjs";
export function previewIdentity(sha, date, artifactBaseUrl) {
if (!/^[a-f0-9]{40}$/.test(sha) || Number.isNaN(date.getTime())) throw new Error("Invalid preview commit");
const base = new URL(artifactBaseUrl);
if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash) throw new Error("Invalid staging artifact base URL");
const day = `${date.getUTCMonth() + 1}${String(date.getUTCDate()).padStart(2, "0")}`;
const second = date.getUTCHours() * 3600 + date.getUTCMinutes() * 60 + date.getUTCSeconds() + 1;
return { tag: `preview/${sha}`, version: `${date.getUTCFullYear()}.${day}.${second}-preview.sha${sha}`,
baseUrl: `${base.href.replace(/\/$/, "")}/${sha}` };
}
export function buildPreviewMigrator(outputDirectory, artifactBaseUrl) {
const repo = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
const git = (...args) => execFileSync("git", args, { cwd: repo, encoding: "utf8" }).trim();
const sha = git("rev-parse", "HEAD");
if (process.env.GITHUB_SHA && process.env.GITHUB_SHA !== sha) throw new Error("Preview checkout differs from the workflow commit");
git("diff", "--quiet", "HEAD");
const identity = previewIdentity(sha, new Date(git("show", "-s", "--format=%cI", "HEAD")), artifactBaseUrl);
execFileSync("pnpm", ["--filter", "@paperclipai/db...", "build"], { cwd: repo, stdio: "inherit" });
const output = path.resolve(outputDirectory);
mkdirSync(output, { recursive: true });
const temporary = mkdtempSync(path.join(os.tmpdir(), "paperclip-preview-migrator-"));
try {
for (const name of ["shared", "db"]) {
const source = path.join(repo, "packages", name);
const staged = path.join(temporary, name);
if (name === "db") prepareBundledPackage(source, staged);
else { mkdirSync(staged); cpSync(path.join(source, "dist"), path.join(staged, "dist"), { recursive: true }); }
const manifest = name === "db" ? JSON.parse(readFileSync(path.join(staged, "package.json"), "utf8"))
: materializePublishManifest(JSON.parse(readFileSync(path.join(source, "package.json"), "utf8")));
manifest.version = identity.version;
manifest.gitHead = sha;
delete manifest.devDependencies;
delete manifest.scripts;
if (name === "db") manifest.dependencies["@paperclipai/shared"] = `${identity.baseUrl}/paperclipai-shared.tgz`;
writeFileSync(path.join(staged, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`);
for (const file of ["LICENSE", "README.md"]) {
if (existsSync(path.join(source, file))) cpSync(path.join(source, file), path.join(staged, file));
}
const result = JSON.parse(execFileSync("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", temporary], { cwd: staged, encoding: "utf8" }));
const destination = path.join(output, `paperclipai-${name}.tgz`);
if (existsSync(destination)) throw new Error("Refusing to overwrite a preview artifact");
renameSync(path.join(temporary, result[0].filename), destination);
}
const integrity = (name) => `sha512-${createHash("sha512").update(readFileSync(path.join(output, `paperclipai-${name}.tgz`))).digest("base64")}`;
const manifest = { version: 1, githubSha: sha, dbPackageVersion: identity.version,
dbPackageIntegrity: integrity("db"), dbPackageTarballUrl: `${identity.baseUrl}/paperclipai-db.tgz`,
sharedPackageIntegrity: integrity("shared"), sharedPackageTarballUrl: `${identity.baseUrl}/paperclipai-shared.tgz` };
writeFileSync(path.join(output, "preview-migrator.json"), `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
return manifest;
} finally { rmSync(temporary, { recursive: true, force: true }); }
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
if (!process.argv[2] || !process.argv[3]) throw new Error("Usage: build-preview-migrator.mjs <new output directory> <staging artifact base URL>");
console.log(JSON.stringify(buildPreviewMigrator(process.argv[2], process.argv[3])));
}