mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Harden work-folder recovery and add deployed staging acceptance tools
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a13bb6b395
commit
6c7d1bbbf0
23 files changed
+656
-38
No files matched your search
@@ -0,0 +1,63 @@
|
||||
#!/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, repository = "paperclipai/paperclip") {
|
||||
if (!/^[a-f0-9]{40}$/.test(sha) || Number.isNaN(date.getTime())) throw new Error("Invalid preview commit");
|
||||
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: `https://github.com/${repository}/releases/download/${encodeURIComponent(`preview/${sha}`)}` };
|
||||
}
|
||||
|
||||
export function buildPreviewMigrator(outputDirectory) {
|
||||
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")));
|
||||
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]) throw new Error("Usage: build-preview-migrator.mjs <new output directory>");
|
||||
console.log(JSON.stringify(buildPreviewMigrator(process.argv[2])));
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { previewIdentity } from "./build-preview-migrator.mjs";
|
||||
|
||||
test("preview artifact identity is immutable, namespaced, and ordered by commit time", () => {
|
||||
const sha = "2f42a4968d5761fd62172e35ecf8188195b8d431";
|
||||
const identity = previewIdentity(sha, new Date("2026-07-19T09:30:00.000Z"));
|
||||
assert.equal(identity.version, `2026.719.34201-preview.sha${sha}`);
|
||||
assert.equal(identity.tag, `preview/${sha}`);
|
||||
assert.equal(identity.baseUrl, `https://github.com/paperclipai/paperclip/releases/download/preview%2F${sha}`);
|
||||
assert.throws(() => previewIdentity("master", new Date()), /Invalid preview/);
|
||||
assert.throws(() => previewIdentity(sha, new Date("invalid")), /Invalid preview/);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, readdirSync, statSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, readdirSync, realpathSync, statSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -275,7 +275,9 @@ function runVitest(args, label) {
|
||||
console.log(`\n[test:run] ${label}`);
|
||||
invocationIndex += 1;
|
||||
const tempRootParent = process.platform === "win32" ? os.tmpdir() : "/tmp";
|
||||
const testRoot = mkdtempSync(path.join(tempRootParent, `pcvt-${process.pid}-${invocationIndex}-`));
|
||||
// /tmp is a symlink on macOS. Workspace confinement intentionally rejects
|
||||
// aliases, so fixtures must receive the canonical root just as Linux does.
|
||||
const testRoot = realpathSync(mkdtempSync(path.join(tempRootParent, `pcvt-${process.pid}-${invocationIndex}-`)));
|
||||
// Keep per-run paths compact so Unix socket fixtures stay under macOS path limits.
|
||||
const env = {
|
||||
...process.env,
|
||||
|
||||
Reference in new issue
Block a user