fix(verify): isolate package archives and published canary caches
This commit is contained in:
parent
90b06db6c0
commit
3eb7dbdc16
|
|
@ -6,7 +6,6 @@ import { exec, json, run } from "./lib/verify-package-core.mjs";
|
|||
|
||||
/** Install the packed CLI globally with and without optional dependencies and verify PTY degradation. */
|
||||
export async function verifyPackageInstall({
|
||||
root,
|
||||
tarball,
|
||||
prefix,
|
||||
workspace,
|
||||
|
|
@ -14,14 +13,7 @@ export async function verifyPackageInstall({
|
|||
}) {
|
||||
await exec(
|
||||
"npm",
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
"--ignore-scripts",
|
||||
"--prefix",
|
||||
prefix,
|
||||
join(root, tarball),
|
||||
],
|
||||
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
|
||||
{ env: environment },
|
||||
);
|
||||
const cli = join(prefix, "bin", "rea");
|
||||
|
|
@ -61,7 +53,7 @@ export async function verifyPackageInstall({
|
|||
"--omit=optional",
|
||||
"--prefix",
|
||||
noOptionalPrefix,
|
||||
join(root, tarball),
|
||||
tarball,
|
||||
],
|
||||
{ env: environment },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
|
||||
|
||||
|
|
@ -6,15 +7,23 @@ import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
|
|||
|
||||
/** Create a tarball and assert production packaging constraints. */
|
||||
export async function verifyPackagePack({ root, workspace }) {
|
||||
const tarball = (
|
||||
await exec("npm", ["pack", "--silent"], { cwd: root })
|
||||
).stdout.trim();
|
||||
const packedFiles = (
|
||||
await exec("tar", ["-tf", join(root, tarball)])
|
||||
).stdout.split("\n");
|
||||
const packDirectory = join(workspace, "pack");
|
||||
await mkdir(packDirectory);
|
||||
const tarball = join(
|
||||
packDirectory,
|
||||
(
|
||||
await exec(
|
||||
"npm",
|
||||
["pack", "--silent", "--pack-destination", packDirectory],
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
)
|
||||
).stdout.trim(),
|
||||
);
|
||||
const packedFiles = (await exec("tar", ["-tf", tarball])).stdout.split("\n");
|
||||
const packedManifest = JSON.parse(
|
||||
(await exec("tar", ["-xOf", join(root, tarball), "package/package.json"]))
|
||||
.stdout,
|
||||
(await exec("tar", ["-xOf", tarball, "package/package.json"])).stdout,
|
||||
);
|
||||
if (
|
||||
packedManifest.scripts?.postinstall !== undefined ||
|
||||
|
|
@ -42,7 +51,7 @@ export async function verifyPackagePack({ root, workspace }) {
|
|||
const skill = (
|
||||
await exec("tar", [
|
||||
"-xOf",
|
||||
join(root, tarball),
|
||||
tarball,
|
||||
"package/skills/reverse-engineer-anything/SKILL.md",
|
||||
])
|
||||
).stdout;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ try {
|
|||
referenceRoot,
|
||||
});
|
||||
const { cli, packageRunnerCli } = await verifyPackageInstall({
|
||||
root,
|
||||
tarball,
|
||||
prefix: environmentData.prefix,
|
||||
workspace,
|
||||
|
|
@ -58,7 +57,7 @@ try {
|
|||
"node_modules",
|
||||
PRODUCT_IDENTITY.packageName,
|
||||
),
|
||||
tarball: join(root, tarball),
|
||||
tarball,
|
||||
workspace,
|
||||
environment: environmentData.environment,
|
||||
});
|
||||
|
|
@ -137,6 +136,5 @@ try {
|
|||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 4 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, update, mcpStartup, mcpModuleLoading })}\n`,
|
||||
);
|
||||
} finally {
|
||||
if (tarball) await rm(join(root, tarball), { force: true });
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function verifyPackedBridge({
|
|||
throw new Error("package omitted the Hopper bridge");
|
||||
if (!packedFiles.includes(packedGhidraBridge))
|
||||
throw new Error("package omitted the Ghidra bridge");
|
||||
await exec("tar", ["-xf", join(root, tarball), "-C", workspace]);
|
||||
await exec("tar", ["-xf", tarball, "-C", workspace]);
|
||||
const ghidraSource = await readFile(
|
||||
join(workspace, packedGhidraBridge),
|
||||
"utf8",
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ const version = process.argv[2];
|
|||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version ?? ""))
|
||||
throw new Error("Usage: node scripts/verify-published-package.mjs <version>");
|
||||
|
||||
const serverEnvironment = { ...process.env };
|
||||
delete serverEnvironment.HOPPER_TARGET_PATH;
|
||||
const canaryRoot = await mkdtemp(join(tmpdir(), "rea-published-canary-"));
|
||||
const serverEnvironment = {
|
||||
...process.env,
|
||||
npm_config_cache: join(canaryRoot, "npm-cache"),
|
||||
};
|
||||
delete serverEnvironment.HOPPER_TARGET_PATH;
|
||||
const transport = new StdioClientTransport({
|
||||
command: "npm",
|
||||
args: [
|
||||
|
|
@ -91,7 +94,6 @@ process.stdout.write(
|
|||
);
|
||||
|
||||
async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
|
||||
const previousVersion = await findPreviousPublishedVersion(targetVersion);
|
||||
const prefix = join(canaryRoot, "update-prefix");
|
||||
const home = join(canaryRoot, "update-home");
|
||||
const environment = {
|
||||
|
|
@ -101,6 +103,11 @@ async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
|
|||
npm_config_prefix: prefix,
|
||||
PATH: `${join(prefix, "bin")}${delimiter}${process.env.PATH ?? ""}`,
|
||||
};
|
||||
const previousVersion = await findPreviousPublishedVersion(
|
||||
targetVersion,
|
||||
canaryRoot,
|
||||
environment,
|
||||
);
|
||||
|
||||
await execFileAsync(
|
||||
"npm",
|
||||
|
|
@ -193,11 +200,11 @@ if (result.status === "failed") process.exitCode = 1;
|
|||
};
|
||||
}
|
||||
|
||||
async function findPreviousPublishedVersion(targetVersion) {
|
||||
async function findPreviousPublishedVersion(targetVersion, cwd, environment) {
|
||||
const { stdout } = await execFileAsync(
|
||||
"npm",
|
||||
["view", "rea-agents", "versions", "--json"],
|
||||
{ maxBuffer: 16 * 1024 * 1024 },
|
||||
{ cwd, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const versions = JSON.parse(stdout);
|
||||
const previous = versions
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const expectedFailure =
|
||||
"package did not retain its MCP Registry ownership marker";
|
||||
|
||||
it("keeps npm pack output in the owned workspace when package validation fails", async () => {
|
||||
const testRoot = await createTestTempDirectory("rea-pack-isolation-");
|
||||
const packageRoot = join(testRoot, "source");
|
||||
const workspace = join(testRoot, "workspace");
|
||||
const npmCache = join(testRoot, "npm-cache");
|
||||
const archiveName = "rea-agents-6.0.0.tgz";
|
||||
const archivePath = join(packageRoot, archiveName);
|
||||
const sentinel = Buffer.from("caller-owned archive\n");
|
||||
await Promise.all([mkdir(packageRoot), mkdir(workspace), mkdir(npmCache)]);
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(packageRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "rea-agents",
|
||||
version: "6.0.0",
|
||||
mcpName: "invalid-owner",
|
||||
bin: { rea: "scripts/rea.mjs", "rea-agents": "scripts/rea.mjs" },
|
||||
dependencies: { "@lydell/node-pty": "1.1.0" },
|
||||
}),
|
||||
),
|
||||
writeFile(archivePath, sentinel),
|
||||
]);
|
||||
const packageContentsBefore = (await readdir(packageRoot)).sort();
|
||||
const packerUrl = pathToFileURL(
|
||||
resolve("scripts/verify-package-pack.mjs"),
|
||||
).href;
|
||||
const harness = `
|
||||
import { rm } from "node:fs/promises";
|
||||
const workspace = ${JSON.stringify(workspace)};
|
||||
let failure;
|
||||
try {
|
||||
const { verifyPackagePack } = await import(${JSON.stringify(packerUrl)});
|
||||
await verifyPackagePack({ root: ${JSON.stringify(packageRoot)}, workspace });
|
||||
} catch (cause) {
|
||||
if (!(cause instanceof Error) || cause.message !== ${JSON.stringify(expectedFailure)}) throw cause;
|
||||
failure = cause.message;
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
if (failure !== ${JSON.stringify(expectedFailure)}) throw new Error("Expected package metadata validation to fail after packing");
|
||||
process.stdout.write(JSON.stringify({ failure }));
|
||||
`;
|
||||
const { stdout } = await execFileAsync(
|
||||
process.execPath,
|
||||
["--input-type=module", "--eval", harness],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_cache: npmCache,
|
||||
TEMP: npmCache,
|
||||
TMP: npmCache,
|
||||
TMPDIR: npmCache,
|
||||
},
|
||||
maxBuffer: 1_024 * 1_024,
|
||||
},
|
||||
);
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual({ failure: expectedFailure });
|
||||
expect((await readdir(packageRoot)).sort()).toEqual(packageContentsBefore);
|
||||
expect(await readFile(archivePath)).toEqual(sentinel);
|
||||
await expect(access(workspace)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
Loading…
Reference in New Issue