Files
2026-07-13 13:39:12 +08:00

129 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
PACK_ARTIFACT_ALLOWED_EXACT_PATHS,
PACK_ARTIFACT_ALLOWED_PATH_PREFIXES,
PACK_ARTIFACT_REQUIRED_PATHS,
findMissingArtifactPaths,
findUnexpectedArtifactPaths,
} from "./pack-artifact-policy.ts";
const __filename: string = fileURLToPath(import.meta.url);
const __dirname: string = dirname(__filename);
const ROOT: string = join(__dirname, "..", "..");
const npmCommand: string = process.platform === "win32" ? "npm.cmd" : "npm";
function runNpm(args: string[], stdio: "inherit" | "pipe" = "pipe"): string {
const npmExecPath = process.env.npm_execpath;
const isBunRuntime = "Bun" in globalThis;
const command = npmExecPath && !isBunRuntime ? process.execPath : npmCommand;
const commandArgs = npmExecPath && !isBunRuntime ? [npmExecPath, ...args] : args;
return execFileSync(command, commandArgs, {
cwd: ROOT,
encoding: "utf8",
stdio: stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"],
maxBuffer: 64 * 1024 * 1024,
});
}
function ensureAppStagingReady(): void {
const missingAppRequiredPaths = PACK_ARTIFACT_REQUIRED_PATHS.filter((requiredPath) =>
requiredPath.startsWith("dist/")
).filter((requiredPath) => !existsSync(join(ROOT, requiredPath)));
if (missingAppRequiredPaths.length === 0) return;
console.log("📦 dist/ staging is missing required runtime files; running npm run build:cli...");
runNpm(["run", "build:cli"], "inherit");
}
function runPackDryRun(): any {
const output = runNpm(["pack", "--dry-run", "--json", "--ignore-scripts"]);
const jsonStart = output.indexOf("[");
const jsonEnd = output.lastIndexOf("]");
const jsonPayload =
jsonStart >= 0 && jsonEnd > jsonStart ? output.slice(jsonStart, jsonEnd + 1) : output;
const parsed = JSON.parse(jsonPayload);
const packReport = Array.isArray(parsed) ? parsed[0] : null;
if (!packReport || !Array.isArray(packReport.files)) {
throw new Error("npm pack --dry-run --json did not return the expected files[] payload.");
}
return packReport;
}
function formatBytes(bytes: number): string {
if (!Number.isFinite(bytes) || bytes < 1024) {
return `${bytes || 0} B`;
}
const units = ["KB", "MB", "GB"];
let value = bytes / 1024;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`;
}
// --policy-only: skip the build (ensureAppStagingReady → build:cli) and the
// required-runtime-files check (which needs the built dist/), running ONLY the
// unexpected-files allowlist check. The unexpected files (e.g. stray bin/*.sh) are
// SOURCE files that `npm pack --dry-run` lists regardless of build, so this catches
// the "new file leaked into the tarball" regression cheaply on the fast-path (PR→release),
// instead of only on the release PR's full Package Artifact job. See incident v3.8.36 (#5029).
const POLICY_ONLY = process.argv.includes("--policy-only");
try {
if (!POLICY_ONLY) ensureAppStagingReady();
const packReport = runPackDryRun();
const artifactPaths: string[] = packReport.files.map((file: any) => file.path);
const unexpectedPaths: string[] = findUnexpectedArtifactPaths(artifactPaths, {
exactPaths: PACK_ARTIFACT_ALLOWED_EXACT_PATHS,
prefixPaths: PACK_ARTIFACT_ALLOWED_PATH_PREFIXES,
});
const missingRequiredPaths: string[] = POLICY_ONLY
? []
: findMissingArtifactPaths(artifactPaths, PACK_ARTIFACT_REQUIRED_PATHS);
console.log("📦 npm pack artifact summary");
console.log(` File: ${packReport.filename}`);
console.log(` Entry count: ${packReport.entryCount}`);
console.log(` Packed size: ${formatBytes(packReport.size)}`);
console.log(` Unpacked size: ${formatBytes(packReport.unpackedSize)}`);
if (unexpectedPaths.length > 0) {
console.error("\n❌ Unexpected files were found in the npm publish artifact:");
for (const unexpectedPath of unexpectedPaths) {
console.error(` - ${unexpectedPath}`);
}
}
if (missingRequiredPaths.length > 0) {
console.error("\n❌ Required runtime files are missing from the npm publish artifact:");
for (const missingPath of missingRequiredPaths) {
console.error(` - ${missingPath}`);
}
}
if (unexpectedPaths.length > 0 || missingRequiredPaths.length > 0) {
process.exit(1);
}
console.log("\n✅ Pack artifact policy check passed.");
} catch (error) {
console.error(`\n❌ Pack artifact validation failed: ${error.message}`);
process.exit(1);
}