build: exclude private QA from npm package

This commit is contained in:
Peter Steinberger
2026-04-15 09:38:45 -07:00
parent 78ac118427
commit 229eb72cf6
30 changed files with 539 additions and 86 deletions

View File

@@ -19,7 +19,6 @@ const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = BUNDLED_RUNTIME_SIDECAR_PATHS.fi
);
const LEGACY_UPDATE_COMPAT_RUNTIME_SIDECAR_PATHS = [
"dist/extensions/qa-channel/runtime-api.js",
"dist/extensions/qa-lab/runtime-api.js",
] as const;
const REQUIRED_INSTALLED_RUNTIME_SIDECAR_PATHS = [
...PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS,
@@ -316,7 +315,7 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
}
});
it("rejects private qa sidecar directories that are missing package.json", () => {
it("rejects packaged qa channel sidecar directories that are missing package.json", () => {
const packageRoot = makeInstalledPackageRoot();
try {
@@ -325,21 +324,14 @@ describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
dependencies: {},
});
mkdirSync(join(packageRoot, "dist/extensions/qa-channel"), { recursive: true });
mkdirSync(join(packageRoot, "dist/extensions/qa-lab"), { recursive: true });
writeFileSync(
join(packageRoot, "dist/extensions/qa-channel/runtime-api.js"),
"export {};\n",
"utf8",
);
writeFileSync(
join(packageRoot, "dist/extensions/qa-lab/runtime-api.js"),
"export {};\n",
"utf8",
);
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
`installed bundled extension manifest missing: ${join(packageRoot, "dist/extensions/qa-channel/package.json")}.`,
`installed bundled extension manifest missing: ${join(packageRoot, "dist/extensions/qa-lab/package.json")}.`,
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });

View File

@@ -1,8 +1,12 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { WORKSPACE_TEMPLATE_PACK_PATHS } from "../scripts/lib/workspace-bootstrap-smoke.mjs";
import {
compareReleaseVersions,
collectControlUiPackErrors,
collectForbiddenPackedContentErrors,
collectForbiddenPackedPathErrors,
collectReleasePackageMetadataErrors,
collectReleaseTagErrors,
@@ -15,10 +19,13 @@ import {
shouldSkipPackedTarballValidation,
utcCalendarDayDistance,
} from "../scripts/openclaw-npm-release-check.ts";
import { PACKAGE_DIST_INVENTORY_RELATIVE_PATH } from "../src/infra/package-dist-inventory.ts";
const LEGACY_UPDATE_COMPAT_PACKED_PATHS = [
"dist/extensions/qa-channel/runtime-api.js",
"dist/extensions/qa-lab/runtime-api.js",
const LEGACY_UPDATE_COMPAT_PACKED_PATHS = ["dist/extensions/qa-channel/runtime-api.js"] as const;
const REQUIRED_PACKED_PATHS = [
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
...LEGACY_UPDATE_COMPAT_PACKED_PATHS,
...WORKSPACE_TEMPLATE_PACK_PATHS,
] as const;
describe("parseReleaseVersion", () => {
@@ -286,11 +293,7 @@ describe("parseNpmPackJsonOutput", () => {
describe("collectControlUiPackErrors", () => {
it("rejects packs that ship the dashboard HTML without the asset payload", () => {
expect(collectControlUiPackErrors(["dist/control-ui/index.html"])).toEqual([
...LEGACY_UPDATE_COMPAT_PACKED_PATHS.map(
(requiredPath) =>
`npm package is missing required path "${requiredPath}". Ensure UI assets are built and included before publish.`,
),
...WORKSPACE_TEMPLATE_PACK_PATHS.map(
...REQUIRED_PACKED_PATHS.map(
(requiredPath) =>
`npm package is missing required path "${requiredPath}". Ensure UI assets are built and included before publish.`,
),
@@ -302,8 +305,7 @@ describe("collectControlUiPackErrors", () => {
expect(
collectControlUiPackErrors([
"dist/control-ui/index.html",
...LEGACY_UPDATE_COMPAT_PACKED_PATHS,
...WORKSPACE_TEMPLATE_PACK_PATHS,
...REQUIRED_PACKED_PATHS,
"dist/control-ui/assets/index-Bu8rSoJV.js",
"dist/control-ui/assets/index-BK0yXA_h.css",
]),
@@ -332,20 +334,49 @@ describe("collectForbiddenPackedPathErrors", () => {
"dist/extensions/qa-channel/package.json",
"dist/extensions/qa-lab/runtime-api.js",
"dist/extensions/qa-lab/src/cli.js",
"dist/plugin-sdk/extensions/qa-lab/cli.d.ts",
"dist/qa-runtime-B9LDtssJ.js",
"qa/scenarios/index.md",
]),
).toEqual([
'npm package must not include private QA channel artifact "dist/extensions/qa-channel/package.json".',
'npm package must not include private QA lab artifact "dist/extensions/qa-lab/runtime-api.js".',
'npm package must not include private QA lab artifact "dist/extensions/qa-lab/src/cli.js".',
'npm package must not include private QA lab type artifact "dist/plugin-sdk/extensions/qa-lab/cli.d.ts".',
'npm package must not include private QA runtime chunk "dist/qa-runtime-B9LDtssJ.js".',
'npm package must not include private QA suite artifact "qa/scenarios/index.md".',
]);
});
it("allows the legacy update verifier QA runtime sidecars", () => {
it("allows only the legacy update verifier QA channel runtime sidecar", () => {
expect(
collectForbiddenPackedPathErrors([
"dist/extensions/qa-channel/runtime-api.js",
"dist/extensions/qa-lab/runtime-api.js",
]),
).toEqual([]);
).toEqual([
'npm package must not include private QA lab artifact "dist/extensions/qa-lab/runtime-api.js".',
]);
});
it("rejects root dist chunks that still reference the private qa lab", () => {
const rootDir = mkdtempSync(join(tmpdir(), "openclaw-pack-private-qa-"));
try {
mkdirSync(join(rootDir, "dist"), { recursive: true });
writeFileSync(
join(rootDir, "dist", "entry.js"),
"//#region extensions/qa-lab/src/cli.ts\n",
"utf8",
);
writeFileSync(join(rootDir, "README.md"), "developer docs mention extensions/qa-lab/\n");
expect(collectForbiddenPackedContentErrors(["dist/entry.js", "README.md"], rootDir)).toEqual([
'npm package must not include private QA lab marker "//#region extensions/qa-lab/" in "dist/entry.js".',
]);
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
});

View File

@@ -9,13 +9,14 @@ import {
collectAppcastSparkleVersionErrors,
collectBundledExtensionManifestErrors,
collectBundledPluginRootRuntimeMirrorErrors,
collectForbiddenPackContentPaths,
collectRootDistBundledRuntimeMirrors,
collectForbiddenPackPaths,
collectMissingPackPaths,
collectPackUnpackedSizeErrors,
listRequiredQaScenarioPackPaths,
packageNameFromSpecifier,
} from "../scripts/release-check.ts";
import { PACKAGE_DIST_INVENTORY_RELATIVE_PATH } from "../src/infra/package-dist-inventory.ts";
import { bundledDistPluginFile, bundledPluginFile } from "./helpers/bundled-plugin-paths.js";
function makeItem(shortVersion: string, sparkleVersion: string): string {
@@ -28,7 +29,6 @@ function makePackResult(filename: string, unpackedSize: number) {
const requiredPluginSdkPackPaths = [...listPluginSdkDistArtifacts(), "dist/plugin-sdk/compat.js"];
const requiredBundledPluginPackPaths = listBundledPluginPackArtifacts();
const requiredQaScenarioPackPaths = listRequiredQaScenarioPackPaths();
describe("collectAppcastSparkleVersionErrors", () => {
it("accepts legacy 9-digit calver builds before lane-floor cutover", () => {
@@ -309,6 +309,47 @@ describe("collectForbiddenPackPaths", () => {
"dist/plugin-sdk/.tsbuildinfo",
]);
});
it("blocks private qa lab and suite paths from npm pack output", () => {
expect(
collectForbiddenPackPaths([
"dist/index.js",
"dist/extensions/qa-lab/runtime-api.js",
"dist/plugin-sdk/extensions/qa-lab/cli.d.ts",
"dist/plugin-sdk/qa-lab.js",
"dist/plugin-sdk/qa-runtime.js",
"dist/qa-runtime-B9LDtssJ.js",
"qa/scenarios/index.md",
]),
).toEqual([
"dist/extensions/qa-lab/runtime-api.js",
"dist/plugin-sdk/extensions/qa-lab/cli.d.ts",
"dist/plugin-sdk/qa-lab.js",
"dist/plugin-sdk/qa-runtime.js",
"dist/qa-runtime-B9LDtssJ.js",
"qa/scenarios/index.md",
]);
});
it("blocks root dist chunks that still reference private qa lab sources", () => {
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-release-private-qa-"));
try {
mkdirSync(join(tempRoot, "dist"), { recursive: true });
writeFileSync(
join(tempRoot, "dist", "entry.js"),
"//#region extensions/qa-lab/src/runtime-api.ts\n",
"utf8",
);
writeFileSync(join(tempRoot, "CHANGELOG.md"), "local QA notes mention extensions/qa-lab/\n");
expect(collectForbiddenPackContentPaths(["dist/entry.js", "CHANGELOG.md"], tempRoot)).toEqual(
["dist/entry.js"],
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
});
describe("collectMissingPackPaths", () => {
@@ -326,8 +367,8 @@ describe("collectMissingPackPaths", () => {
expect(missing).toEqual(
expect.arrayContaining([
"dist/channel-catalog.json",
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
"dist/control-ui/index.html",
"qa/scenarios/index.md",
"scripts/npm-runner.mjs",
"scripts/preinstall-package-manager-warning.mjs",
"scripts/postinstall-bundled-plugins.mjs",
@@ -343,9 +384,6 @@ describe("collectMissingPackPaths", () => {
bundledDistPluginFile("whatsapp", "package.json"),
]),
);
expect(
missing.some((path) => path.startsWith("qa/scenarios/") && path !== "qa/scenarios/index.md"),
).toBe(true);
});
it("accepts the shipped upgrade surface when optional bundled metadata is present", () => {
@@ -357,7 +395,6 @@ describe("collectMissingPackPaths", () => {
"dist/extensions/acpx/mcp-proxy.mjs",
bundledDistPluginFile("diffs", "assets/viewer-runtime.js"),
...requiredBundledPluginPackPaths,
...requiredQaScenarioPackPaths,
...requiredPluginSdkPackPaths,
...WORKSPACE_TEMPLATE_PACK_PATHS,
"scripts/npm-runner.mjs",
@@ -366,6 +403,7 @@ describe("collectMissingPackPaths", () => {
"dist/plugin-sdk/root-alias.cjs",
"dist/build-info.json",
"dist/channel-catalog.json",
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
]),
).toEqual([]);
});
@@ -381,15 +419,6 @@ describe("collectMissingPackPaths", () => {
]),
);
});
it("requires the authored qa scenario pack files in npm pack output", () => {
expect(requiredQaScenarioPackPaths).toContain("qa/scenarios/index.md");
expect(
requiredQaScenarioPackPaths.some(
(path) => path.startsWith("qa/scenarios/") && path !== "qa/scenarios/index.md",
),
).toBe(true);
});
});
describe("collectPackUnpackedSizeErrors", () => {

View File

@@ -6,6 +6,7 @@ import {
pruneInstalledPackageDist,
discoverBundledPluginRuntimeDeps,
pruneBundledPluginSourceNodeModules,
restoreLegacyUpdaterCompatSidecars,
runBundledPluginPostinstall,
} from "../../scripts/postinstall-bundled-plugins.mjs";
import { writePackageDistInventory } from "../../src/infra/package-dist-inventory.ts";
@@ -214,6 +215,63 @@ describe("bundled plugin postinstall", () => {
await expect(fs.stat(staleFile)).rejects.toMatchObject({ code: "ENOENT" });
});
it("restores only postinstall-generated QA lab compat sidecar after pruning old installs", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-qa-compat-");
const currentFile = path.join(packageRoot, "dist", "entry.js");
const stalePackage = path.join(packageRoot, "dist", "extensions", "qa-lab", "package.json");
const staleManifest = path.join(
packageRoot,
"dist",
"extensions",
"qa-lab",
"openclaw.plugin.json",
);
await fs.mkdir(path.dirname(stalePackage), { recursive: true });
await fs.writeFile(currentFile, "export {};\n");
await writePackageDistInventory(packageRoot);
await fs.writeFile(stalePackage, "{}\n");
await fs.writeFile(staleManifest, "{}\n");
runBundledPluginPostinstall({
packageRoot,
spawnSync: vi.fn(),
log: { log: vi.fn(), warn: vi.fn() },
});
await expect(fs.stat(stalePackage)).rejects.toMatchObject({ code: "ENOENT" });
await expect(fs.stat(staleManifest)).rejects.toMatchObject({ code: "ENOENT" });
await expect(
fs.readFile(path.join(packageRoot, "dist", "extensions", "qa-lab", "runtime-api.js"), "utf8"),
).resolves.toContain("QA Lab is not packaged");
});
it("creates only an empty QA lab compat sidecar for fresh installs", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-no-qa-compat-");
await fs.mkdir(path.join(packageRoot, "dist"), { recursive: true });
await fs.writeFile(path.join(packageRoot, "dist", "entry.js"), "export {};\n");
await writePackageDistInventory(packageRoot);
expect(
restoreLegacyUpdaterCompatSidecars({
packageRoot,
removedFiles: ["dist/entry-old.js"],
log: { log: vi.fn(), warn: vi.fn() },
}),
).toEqual(["dist/extensions/qa-lab/runtime-api.js"]);
await expect(
fs.readFile(path.join(packageRoot, "dist", "extensions", "qa-lab", "runtime-api.js"), "utf8"),
).resolves.toBe(
"// Compatibility stub for older OpenClaw updaters. QA Lab is not packaged.\nexport {};\n",
);
await expect(
fs.stat(path.join(packageRoot, "dist", "extensions", "qa-lab", "package.json")),
).rejects.toMatchObject({ code: "ENOENT" });
await expect(
fs.stat(path.join(packageRoot, "dist", "extensions", "qa-lab", "openclaw.plugin.json")),
).rejects.toMatchObject({ code: "ENOENT" });
});
it("keeps packaged postinstall non-fatal when the dist inventory is missing", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-missing-inventory-");
const staleFile = path.join(packageRoot, "dist", "channel-CJUAgRQR.js");

View File

@@ -53,6 +53,13 @@ describe("test-install-sh-docker", () => {
expect(script).toContain('from "./scripts/lib/npm-pack-budget.mjs"');
expect(script).toContain("install smoke cannot verify pack budget");
});
it("writes the package dist inventory before packing ignore-scripts tarballs", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain("node --import tsx scripts/write-package-dist-inventory.ts");
expect(script).toContain("quiet_npm pack --ignore-scripts");
});
});
describe("install-sh smoke runner", () => {