311 lines
19 KiB
JavaScript
311 lines
19 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { planArtifacts } from "./preview-artifacts.mjs";
|
|
import { readFileSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { gzipSync } from "node:zlib";
|
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { previewManifest, assertMetadata, validateRequest, versionFor, tarManifest, packageExists, imageExists, publishPreview, publishImage } from "./preview-artifacts.mjs";
|
|
|
|
const sha = "a".repeat(40);
|
|
const id = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
|
const manifest = (name) => previewManifest({ name, version: "0.0.0", dependencies: name.endsWith("/db") ? { "@paperclipai/shared": "workspace:*" } : {}, publishConfig: { exports: { ".": "./dist/index.js" } } }, sha);
|
|
function pack(pkg) {
|
|
const b = Buffer.from(JSON.stringify(pkg)); const h = Buffer.alloc(512);
|
|
h.write("package/package.json"); h.write(b.length.toString(8).padStart(11, "0"), 124, 11); h[156] = 48;
|
|
const padded = Buffer.alloc(Math.ceil(b.length / 512) * 512); b.copy(padded);
|
|
return gzipSync(Buffer.concat([h, padded, Buffer.alloc(1024)]));
|
|
}
|
|
const json = (body, status = 200) => new Response(JSON.stringify(body), { status });
|
|
|
|
test("preview request requires immutable SHA and correlation UUID", () => {
|
|
validateRequest(sha, id);
|
|
for (const ref of ["master", "origin/master", "a".repeat(7), "$(unsafe)", "A".repeat(40)]) assert.throws(() => versionFor(ref));
|
|
assert.throws(() => validateRequest(sha, "not-a-request"));
|
|
});
|
|
|
|
test("migrator-only planning never waits for GHCR and reuses complete exact-source packages", async () => {
|
|
for (const available of [[], ["@paperclipai/shared"], ["@paperclipai/shared", "@paperclipai/db"]]) {
|
|
const calls = [];
|
|
const result = await planArtifacts(sha, { image: false, migrator: true, fetchImpl: async (url) => {
|
|
assert.equal(new URL(url).hostname, "registry.npmjs.org");
|
|
const name = decodeURIComponent(new URL(url).pathname.split("/")[1]);
|
|
calls.push(name);
|
|
return available.includes(name) ? json({ ...manifest(name), dist: { integrity: "test-integrity", tarball: "https://registry.npmjs.org/package.tgz" } }) : json({}, 404);
|
|
} });
|
|
assert.deepEqual(result, { image: false, packages: available.length !== 2 });
|
|
assert.ok(calls.includes("@paperclipai/shared"));
|
|
if (available.length) assert.ok(calls.includes("@paperclipai/db"));
|
|
}
|
|
});
|
|
|
|
test("migrator-only planning rejects registry outages and mismatched source identity", async () => {
|
|
for (const response of [json({}, 403), json({}, 503), json({ ...manifest("@paperclipai/shared"), gitHead: "b".repeat(40) })]) {
|
|
await assert.rejects(planArtifacts(sha, { image: false, migrator: true, fetchImpl: async () => response }));
|
|
}
|
|
});
|
|
|
|
test("ordinary preview planning still requests a missing image without publishing unsolicited packages", async () => {
|
|
const result = await planArtifacts(sha, { fetchImpl: async (url) => {
|
|
assert.equal(new URL(url).hostname, "ghcr.io");
|
|
return url.includes("/token?") ? json({ token: "test-pull-token" }) : json({}, 404);
|
|
} });
|
|
assert.deepEqual(result, { image: true, packages: false });
|
|
});
|
|
|
|
test("preview manifests carry exact source, isolated versions and shared dependency", () => {
|
|
const pkg = manifest("@paperclipai/db");
|
|
assert.equal(pkg.version, `0.0.0-preview.g${sha}`);
|
|
assert.equal(pkg.dependencies["@paperclipai/shared"], pkg.version);
|
|
assert.deepEqual(pkg.exports, { ".": "./dist/index.js" });
|
|
assertMetadata(pkg, "@paperclipai/db", sha);
|
|
assert.throws(() => assertMetadata({ ...pkg, gitHead: "b".repeat(40) }, pkg.name, sha));
|
|
assert.throws(() => assertMetadata({ ...pkg, dependencies: { "@paperclipai/shared": "latest" } }, pkg.name, sha));
|
|
assert.deepEqual(tarManifest(pack(pkg)), pkg);
|
|
});
|
|
|
|
test("only 404 means an artifact is missing; auth and outages are fatal", async () => {
|
|
assert.equal(await packageExists("@paperclipai/db", sha, async () => json({}, 404)), false);
|
|
await assert.rejects(packageExists("@paperclipai/db", sha, async () => json({}, 403)));
|
|
await assert.rejects(packageExists("@paperclipai/db", sha, async () => json({}, 503)));
|
|
await assert.rejects(imageExists(sha, async () => json({}, 503)));
|
|
assert.equal(await imageExists(sha, async (url) => url.includes("/token?") ? json({ token: "test-pull-token" }) : json({}, 404)), false);
|
|
await assert.rejects(packageExists("@paperclipai/db", sha, async () => json({ ...manifest("@paperclipai/db"), gitHead: "b".repeat(40) })));
|
|
});
|
|
|
|
test("publishing reuses existing previews and never executes package lifecycle hooks", async () => {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "preview-publish-test-"));
|
|
const published = new Set(["@paperclipai/shared"]);
|
|
const calls = [];
|
|
try {
|
|
for (const short of ["shared", "db"]) writeFileSync(path.join(dir, `${short}.tgz`), pack({ ...manifest(`@paperclipai/${short}`), scripts: { prepublishOnly: "do-not-run" } }));
|
|
await publishPreview(dir, sha, {
|
|
fetchImpl: async (url) => {
|
|
const name = decodeURIComponent(new URL(url).pathname.split("/")[1]);
|
|
return published.has(name) ? json({ ...manifest(name), dist: { integrity: "test-integrity", tarball: "https://registry.npmjs.org/package.tgz" } }) : json({}, 404);
|
|
},
|
|
exec: (command, args) => { calls.push({ command, args }); published.add("@paperclipai/db"); },
|
|
sleep: async () => {},
|
|
});
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].command, "npm");
|
|
assert.ok(calls[0].args.includes("--ignore-scripts"));
|
|
assert.equal(calls[0].args[calls[0].args.indexOf("--tag") + 1], "preview");
|
|
assert.ok(!calls[0].args.includes("canary"));
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("preview workflow separates branch compilation from trusted publishing", () => {
|
|
const workflow = readFileSync(new URL("../.github/workflows/release.yml", import.meta.url), "utf8");
|
|
const builder = workflow.split(" package_preview:")[1].split(" publish_preview:")[0];
|
|
const publisher = workflow.split(" publish_preview:")[1].split(" image_preview:")[0];
|
|
const image = workflow.split(" image_preview:")[1].split(" publish_image_preview:")[0];
|
|
const imagePublisher = workflow.split(" publish_image_preview:")[1].split(" result_preview:")[0];
|
|
assert.doesNotMatch(builder, /id-token: write|packages: write|secrets\./);
|
|
assert.doesNotMatch(publisher, /ref: \$\{\{ inputs.source_ref|working-directory: source|pnpm install/);
|
|
assert.match(publisher, /environment: npm-canary/);
|
|
assert.match(image, /PAPERCLIP_BUILD_COMMIT=\$\{\{ inputs.source_ref \}\}/);
|
|
assert.doesNotMatch(image, /cache-(?:to|from):|canary-cloud|latest-cloud|packages: write|secrets\./);
|
|
assert.doesNotMatch(imagePublisher, /ref: \$\{\{ inputs.source_ref|docker\/build-push-action|pnpm install/);
|
|
assert.match(imagePublisher, /publish-image/);
|
|
assert.match(imagePublisher, /environment: npm-canary/);
|
|
assert.match(imagePublisher, /github.ref == 'refs\/heads\/master'/);
|
|
assert.match(publisher, /github.ref == 'refs\/heads\/master'/);
|
|
assert.doesNotMatch(workflow.split(" verify_canary:")[0], /uses: [^\n]+@v\d/);
|
|
assert.match(workflow, /Stack deploy \{0\} build/);
|
|
});
|
|
|
|
test("merge dispatch uses the existing publisher outside full-release concurrency without claiming image readiness", () => {
|
|
const dispatcher = readFileSync(new URL("../.github/workflows/cloud-artifacts.yml", import.meta.url), "utf8");
|
|
const release = readFileSync(new URL("../.github/workflows/release.yml", import.meta.url), "utf8");
|
|
assert.match(dispatcher, /branches: \[master\]/);
|
|
assert.match(dispatcher, /github.ref == 'refs\/heads\/master'/);
|
|
assert.match(dispatcher, /SOURCE_SHA: \$\{\{ github.sha \}\}/);
|
|
assert.match(dispatcher, /gh workflow run release.yml .*--ref master/);
|
|
assert.match(dispatcher, /--field channel=cloud-migrator/);
|
|
assert.doesNotMatch(dispatcher, /actions\/checkout|id-token: write|packages: write|secrets\./);
|
|
assert.match(release, /\(inputs.channel == 'preview' \|\| inputs.channel == 'cloud-migrator'\) && format\('\{0\}-\{1\}', inputs.channel, inputs.source_ref\)/);
|
|
const publisher = release.split(" publish_preview:")[1].split(" image_preview:")[0];
|
|
assert.match(publisher, /group: preview-package-publish-\$\{\{ inputs.source_ref \}\}/);
|
|
assert.match(publisher, /cancel-in-progress: false/);
|
|
assert.match(release, /PLAN_COMMAND: \$\{\{ inputs.channel == 'cloud-migrator' && 'plan-migrator' \|\| 'plan' \}\}/);
|
|
const result = release.split(" result_preview:")[1].split(" verify_canary:")[0];
|
|
assert.match(result, /always\(\) && inputs.channel == 'preview'/);
|
|
});
|
|
|
|
|
|
test("existing image reuse verifies the full revision behind the immutable tag", async () => {
|
|
const digest = "sha256:" + "b".repeat(64);
|
|
for (const revision of [sha, "c".repeat(40)]) {
|
|
const fetchImpl = async (url) => url.includes("/token?") ? json({ token: "test-pull-token" }) :
|
|
url.includes("/blobs/") ? json({ config: { Labels: { "org.opencontainers.image.revision": revision } } }) :
|
|
url.endsWith(digest) ? json({ config: { digest } }) : json({ manifests: [{ digest, platform: { os: "linux", architecture: "amd64" } }] });
|
|
if (revision === sha) assert.equal(await imageExists(sha, fetchImpl), true);
|
|
else await assert.rejects(imageExists(sha, fetchImpl), /full commit/);
|
|
}
|
|
});
|
|
|
|
|
|
test("image publisher verifies source and platform before pushing exactly one immutable tag", async () => {
|
|
for (const revision of [sha, "c".repeat(40)]) {
|
|
const calls = [];
|
|
const operation = publishImage("preview-image.tar", sha, {
|
|
fetchImpl: async (url) => url.includes("/token?") ? json({ token: "test-pull-token" }) : json({}, 404),
|
|
exec: (command, args) => {
|
|
calls.push({ command, args });
|
|
if (args[0] === "image") return JSON.stringify([{ Id: "sha256:" + "b".repeat(64), Os: "linux", Architecture: "amd64", Config: { Labels: { "org.opencontainers.image.revision": revision } } }]);
|
|
return "";
|
|
},
|
|
});
|
|
if (revision === sha) {
|
|
await operation;
|
|
assert.deepEqual(calls.filter((call) => call.args[0] === "push").map((call) => call.args), [["push", `ghcr.io/paperclipai/paperclip:sha-${sha}-cloud`]]);
|
|
} else { await assert.rejects(operation, /identity/); assert.ok(!calls.some((call) => call.args[0] === "push")); }
|
|
assert.ok(!calls.some((call) => ["run", "build"].includes(call.args[0])));
|
|
}
|
|
});
|
|
|
|
|
|
test("commits sharing a short prefix use separate full-SHA image addresses", async () => {
|
|
const urls = [];
|
|
const fetchImpl = async (url) => { urls.push(url); return url.includes("/token?") ? json({ token: "test-pull-token" }) : json({}, 404); };
|
|
const other = sha.slice(0, 7) + "b".repeat(33);
|
|
await imageExists(sha, fetchImpl);
|
|
await imageExists(other, fetchImpl);
|
|
assert.deepEqual(urls.filter((url) => url.includes("/manifests/")), [sha, other].map((commit) => `https://ghcr.io/v2/paperclipai/paperclip/manifests/sha-${commit}-cloud`));
|
|
});
|
|
|
|
test("cloud builds start per commit and preserve tag promotion dependencies", () => {
|
|
const docker = readFileSync(new URL("../.github/workflows/docker.yml", import.meta.url), "utf8");
|
|
const cloud = readFileSync(new URL("../.github/workflows/docker-cloud.yml", import.meta.url), "utf8");
|
|
const readiness = readFileSync(new URL("../.github/workflows/cloud-readiness.yml", import.meta.url), "utf8");
|
|
assert.match(readiness, /branches: \[master\]/);
|
|
assert.match(readiness, /uses: \.\/\.github\/workflows\/docker-cloud.yml/);
|
|
assert.doesNotMatch(cloud, /^ push:/m);
|
|
assert.match(cloud, /workflow_call:/);
|
|
assert.match(cloud, /group: docker-cloud-\$\{\{ github.sha \}\}/);
|
|
assert.match(cloud, /cancel-in-progress: false/);
|
|
assert.doesNotMatch(cloud, /uses: .*@v\d\b/);
|
|
assert.match(cloud, /cache-to: type=registry,ref=ghcr.io\/\$\{\{ github.repository \}\}:buildcache-cloud-\$\{\{ github.sha \}\},mode=max/);
|
|
const caller = docker.split(" build-and-push-cloud:")[1].split(" promote_canary_channel:")[0];
|
|
assert.match(caller, /if: github.event_name != 'push' \|\| github.ref != 'refs\/heads\/master'/);
|
|
assert.match(caller, /uses: .\/.github\/workflows\/docker-cloud.yml/);
|
|
assert.match(docker.split(" promote_canary_channel:")[1], /needs: \[merge-and-push, build-and-push-cloud\]/);
|
|
const reaping = cloud.indexOf(" - name: Verify cloud PID 1 reaps orphaned processes");
|
|
assert.ok(reaping > cloud.indexOf(" - name: Verify the pushed image resolves the declared Sentry version"));
|
|
assert.ok(reaping < cloud.indexOf(" - name: Publish verified full-SHA cloud tag"));
|
|
});
|
|
|
|
test("cloud builds bake the managed runtime identity and verify it before publication", () => {
|
|
const workflow = readFileSync(new URL("../.github/workflows/docker-cloud.yml", import.meta.url), "utf8");
|
|
const build = workflow.split(" - name: Build and push (cloud)")[1].split(" - name:")[0];
|
|
assert.match(build, /build-args: \|\n\s+USER_UID=1001\n\s+USER_GID=1001\n/);
|
|
const verify = workflow.indexOf(" - name: Verify cloud runtime user");
|
|
assert.ok(verify > workflow.indexOf(" - name: Verify the pushed image resolves the declared Sentry version"));
|
|
assert.ok(verify < workflow.indexOf(" - name: Publish verified full-SHA cloud tag"));
|
|
const step = workflow.slice(verify).split("\n - name:")[0];
|
|
assert.match(step, /IMAGE: ghcr.io\/\$\{\{ github.repository \}\}@\$\{\{ steps.build-cloud.outputs.digest \}\}/);
|
|
assert.doesNotMatch(step, /continue-on-error:|if:/);
|
|
assert.ok(step.indexOf('--entrypoint sh "$IMAGE"') < step.indexOf('-e USER_UID=1001 -e USER_GID=1001'));
|
|
for (const flag of ["u", "g"]) {
|
|
assert.ok(step.includes(`test "$(id -${flag} node)" = 1001`));
|
|
assert.ok(step.includes(`test "$(id -${flag})" = 1001`));
|
|
}
|
|
assert.ok(step.includes('test -w "$PAPERCLIP_HOME"'));
|
|
});
|
|
|
|
test("cloud cache imports are bounded, follow master ancestry, and retain the legacy fallback", () => {
|
|
const workflow = readFileSync(new URL("../.github/workflows/docker-cloud.yml", import.meta.url), "utf8");
|
|
const selector = workflow.indexOf(" - name: Select cloud cache ancestry");
|
|
assert.ok(selector > workflow.indexOf(" - name: Login to GitHub Container Registry"));
|
|
assert.ok(selector > workflow.indexOf(" - name: Set up Docker Buildx"));
|
|
assert.ok(selector < workflow.indexOf(" - name: Build and push (cloud)"));
|
|
assert.match(workflow, /run: node scripts\/select-cloud-cache.mjs/);
|
|
assert.match(workflow, /cache-from: \$\{\{ steps.cloud-cache.outputs.source \}\}/);
|
|
const script = fileURLToPath(new URL("./select-cloud-cache.mjs", import.meta.url));
|
|
const dir = mkdtempSync(path.join(tmpdir(), "cloud-cache-test-"));
|
|
const output = path.join(dir, "output");
|
|
const env = { ...process.env, GIT_AUTHOR_NAME: "Test", GIT_AUTHOR_EMAIL: "test@example.test", GIT_COMMITTER_NAME: "Test", GIT_COMMITTER_EMAIL: "test@example.test" };
|
|
const git = (...args) => execFileSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false", ...args], { cwd: dir, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
try {
|
|
git("init", "--initial-branch=master");
|
|
const commits = [];
|
|
for (let i = 0; i < 12; i++) {
|
|
git("commit", "--allow-empty", "-m", `main ${i}`);
|
|
commits.unshift(git("rev-parse", "HEAD"));
|
|
}
|
|
git("checkout", "-b", "topic", "HEAD~1");
|
|
git("commit", "--allow-empty", "-m", "topic");
|
|
git("checkout", "master");
|
|
git("merge", "--no-ff", "topic", "-m", "merge topic");
|
|
commits.unshift(git("rev-parse", "HEAD"));
|
|
const available = `ghcr.io/paperclipai/paperclip:buildcache-cloud-${commits[2]}`;
|
|
const inspections = path.join(dir, "inspections");
|
|
writeFileSync(path.join(dir, "docker"), `#!/usr/bin/env node
|
|
const fs = require("node:fs");
|
|
fs.appendFileSync(process.env.CACHE_INSPECTIONS, process.argv.at(-1) + "\\n");
|
|
if (process.argv.at(-1) !== process.env.AVAILABLE_CACHE) {
|
|
process.stderr.write("manifest unknown");
|
|
process.exit(1);
|
|
}
|
|
`, { mode: 0o755 });
|
|
const result = spawnSync(process.execPath, [script], {
|
|
cwd: dir, encoding: "utf8", env: {
|
|
...env, PATH: `${dir}${path.delimiter}${env.PATH}`, CACHE_IMAGE: "ghcr.io/paperclipai/paperclip",
|
|
GITHUB_OUTPUT: output, AVAILABLE_CACHE: available, CACHE_INSPECTIONS: inspections,
|
|
},
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(readFileSync(output, "utf8"), `source=type=registry,ref=${available}\n`);
|
|
assert.deepEqual(readFileSync(inspections, "utf8").trim().split("\n"), commits.slice(0, 3).map((commit) => `ghcr.io/paperclipai/paperclip:buildcache-cloud-${commit}`));
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("normal cloud builds publish the checked digest only when source and platform match", () => {
|
|
const workflow = readFileSync(new URL("../.github/workflows/docker-cloud.yml", import.meta.url), "utf8");
|
|
const cloud = workflow.split(" build-and-push-cloud:")[1];
|
|
const verify = cloud.indexOf(" - name: Verify the pushed image resolves the declared Sentry version");
|
|
const publish = cloud.indexOf(" - name: Publish verified full-SHA cloud tag");
|
|
assert.ok(verify >= 0 && publish > verify);
|
|
const verification = cloud.slice(verify, publish);
|
|
assert.match(verification, /IMAGE: ghcr.io\/\$\{\{ github.repository \}\}@\$\{\{ steps.build-cloud.outputs.digest \}\}/);
|
|
assert.doesNotMatch(verification, /continue-on-error:|if: always\(/);
|
|
const step = cloud.slice(publish).split(/\n(?: #| - name:)/)[0];
|
|
assert.doesNotMatch(step, /continue-on-error:|if:/);
|
|
assert.match(step, /FULL_SHA_TAG: ghcr.io\/\$\{\{ github.repository \}\}:sha-\$\{\{ github.sha \}\}-cloud/);
|
|
const script = step.split(" run: |\n")[1].split("\n").map((line) => line.replace(/^ {10}/, "")).join("\n");
|
|
const dir = mkdtempSync(path.join(tmpdir(), "cloud-tag-test-"));
|
|
const image = `ghcr.io/paperclipai/paperclip@sha256:${"b".repeat(64)}`;
|
|
const tag = `ghcr.io/paperclipai/paperclip:sha-${sha}-cloud`;
|
|
try {
|
|
writeFileSync(path.join(dir, "docker"), `#!/bin/sh
|
|
case "$1 $2" in
|
|
'image inspect')
|
|
case "$5" in
|
|
*revision*) printf '%s\\n' "$TEST_REVISION" ;;
|
|
*) printf '%s\\n' "$TEST_PLATFORM" ;;
|
|
esac ;;
|
|
'buildx imagetools') printf '%s\\n' "$@" > "$TEST_CALLS" ;;
|
|
*) exit 99 ;;
|
|
esac
|
|
`, { mode: 0o755 });
|
|
for (const [revision, platform, succeeds] of [[sha, "linux/amd64", true], ["c".repeat(40), "linux/amd64", false], [sha, "linux/arm64", false]]) {
|
|
const calls = path.join(dir, "calls");
|
|
rmSync(calls, { force: true });
|
|
const result = spawnSync("bash", ["-c", script], { encoding: "utf8", env: {
|
|
...process.env, PATH: `${dir}${path.delimiter}${process.env.PATH}`, GITHUB_SHA: sha,
|
|
IMAGE: image, FULL_SHA_TAG: tag, TEST_REVISION: revision, TEST_PLATFORM: platform, TEST_CALLS: calls,
|
|
} });
|
|
if (succeeds) {
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(readFileSync(calls, "utf8").trim().split("\n"), ["buildx", "imagetools", "create", "--prefer-index=false", "--tag", tag, image]);
|
|
} else {
|
|
assert.notEqual(result.status, 0);
|
|
assert.throws(() => readFileSync(calls), { code: "ENOENT" });
|
|
}
|
|
}
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|