#!/usr/bin/env node import { execFileSync, spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { MCP_CLOSURE_SPOT_CHECK_PATH, computeMcpClosure, findLeakedTestArtifactPaths, findMissingMcpClosurePaths, } from "./mcpPublishedFilesClosure.ts"; import { PACK_ARTIFACT_ALLOWED_EXACT_PATHS, PACK_ARTIFACT_ALLOWED_PATH_PREFIXES, PACK_ARTIFACT_REQUIRED_PATHS, findMissingArtifactPaths, findUnexpectedArtifactPaths, parseJsonValuesOutput, } 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; if (stdio === "inherit") { execFileSync(command, commandArgs, { cwd: ROOT, encoding: "utf8", stdio: "inherit", maxBuffer: 64 * 1024 * 1024, }); return ""; } const result = spawnSync(command, commandArgs, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024, }); if (result.error) throw result.error; if (result.status !== 0) { throw new Error( (result.stderr || result.stdout || `npm exited with status ${result.status}`).trim() ); } return `${result.stdout || ""}\n${result.stderr || ""}`; } 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"); } type PackReport = { files: Array<{ path: string }>; filename?: string; entryCount?: number; size?: number; unpackedSize?: number; }; function findPackReport(value: unknown): PackReport | null { if (Array.isArray(value)) { for (const item of value) { const report = findPackReport(item); if (report) return report; } return null; } if (typeof value !== "object" || value === null) return null; const record = value as Record; if (Array.isArray(record.files)) return record as unknown as PackReport; for (const child of Object.values(record)) { const report = findPackReport(child); if (report) return report; } return null; } function runPackDryRun(): PackReport { const output = runNpm(["pack", "--dry-run", "--json", "--ignore-scripts"]); const packReport = parseJsonValuesOutput(output) .map(findPackReport) .find((report): report is PackReport => report !== 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/). Source-side policy checks // still run against the real `npm pack --dry-run` file list: unexpected files (e.g. stray // bin/*.sh), test/spec leaks, and missing MCP closure files. This catches source regressions // 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) => 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); // #3821 — broad `files` prefixes (open-sse/, src/lib/, ...) would otherwise allow // co-located *.test.* / __tests__ leaks; ban them explicitly on the real pack list. const leakedTestPaths: string[] = findLeakedTestArtifactPaths(artifactPaths); // #3578 — MCP runs from published TypeScript source; every reachable file must pack. const mcpClosure: string[] = computeMcpClosure(ROOT); const missingMcpPaths: string[] = findMissingMcpClosurePaths(artifactPaths, mcpClosure); 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)}`); console.log(` MCP closure: ${mcpClosure.length} source files checked`); 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 (leakedTestPaths.length > 0) { console.error( "\n❌ Test/spec files leaked into the npm publish artifact (tighten package.json files negations):" ); for (const leakedPath of leakedTestPaths) { console.error(` - ${leakedPath}`); } } if (missingMcpPaths.length > 0) { console.error( "\n❌ MCP-reachable source files are missing from the npm publish artifact (would 404 --mcp):" ); for (const missingPath of missingMcpPaths) { console.error(` - ${missingPath}`); } if (missingMcpPaths.includes(MCP_CLOSURE_SPOT_CHECK_PATH)) { console.error(` (includes the #3578 bug file ${MCP_CLOSURE_SPOT_CHECK_PATH})`); } } if ( unexpectedPaths.length > 0 || missingRequiredPaths.length > 0 || leakedTestPaths.length > 0 || missingMcpPaths.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); }