mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-20 21:02:10 +08:00
build: exclude private QA from npm package
This commit is contained in:
12
package.json
12
package.json
@@ -33,10 +33,14 @@
|
||||
"!dist/extensions/qa-channel/**",
|
||||
"dist/extensions/qa-channel/runtime-api.js",
|
||||
"!dist/extensions/qa-lab/**",
|
||||
"dist/extensions/qa-lab/runtime-api.js",
|
||||
"!dist/extensions/qa-matrix/**",
|
||||
"!dist/plugin-sdk/extensions/qa-lab/**",
|
||||
"!dist/plugin-sdk/qa-lab.*",
|
||||
"!dist/plugin-sdk/qa-runtime.*",
|
||||
"!dist/plugin-sdk/src/plugin-sdk/qa-lab.d.ts",
|
||||
"!dist/plugin-sdk/src/plugin-sdk/qa-runtime.d.ts",
|
||||
"!dist/qa-runtime-*.js",
|
||||
"docs/reference/templates/",
|
||||
"qa/scenarios/",
|
||||
"skills/",
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/preinstall-package-manager-warning.mjs",
|
||||
@@ -767,10 +771,6 @@
|
||||
"types": "./dist/plugin-sdk/matrix-thread-bindings.d.ts",
|
||||
"default": "./dist/plugin-sdk/matrix-thread-bindings.js"
|
||||
},
|
||||
"./plugin-sdk/qa-runtime": {
|
||||
"types": "./dist/plugin-sdk/qa-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/qa-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/qa-runner-runtime": {
|
||||
"types": "./dist/plugin-sdk/qa-runner-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/qa-runner-runtime.js"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { NON_PACKAGED_BUNDLED_PLUGIN_DIRS } from "./lib/bundled-plugin-build-entries.mjs";
|
||||
import { shouldBuildBundledCluster } from "./lib/optional-bundled-clusters.mjs";
|
||||
import {
|
||||
removeFileIfExists,
|
||||
@@ -12,6 +13,13 @@ const GENERATED_BUNDLED_SKILLS_DIR = "bundled-skills";
|
||||
const TRANSIENT_COPY_ERROR_CODES = new Set(["EEXIST", "ENOENT", "ENOTEMPTY", "EBUSY"]);
|
||||
const COPY_RETRY_DELAYS_MS = [10, 25, 50];
|
||||
|
||||
function shouldCopyBundledPluginMetadata(id, env) {
|
||||
if (!NON_PACKAGED_BUNDLED_PLUGIN_DIRS.has(id)) {
|
||||
return true;
|
||||
}
|
||||
return env.OPENCLAW_BUILD_PRIVATE_QA === "1";
|
||||
}
|
||||
|
||||
export function rewritePackageExtensions(entries) {
|
||||
if (!Array.isArray(entries)) {
|
||||
return undefined;
|
||||
@@ -234,6 +242,10 @@ export function copyBundledPluginMetadata(params = {}) {
|
||||
? JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
|
||||
: undefined;
|
||||
const topLevelPublicSurfaceEntries = collectTopLevelPublicSurfaceEntries(pluginDir);
|
||||
if (!shouldCopyBundledPluginMetadata(dirent.name, env)) {
|
||||
removePathIfExists(distPluginDir);
|
||||
continue;
|
||||
}
|
||||
if (!shouldBuildBundledCluster(dirent.name, env, { packageJson })) {
|
||||
removePathIfExists(distPluginDir);
|
||||
continue;
|
||||
|
||||
@@ -10,6 +10,7 @@ export type BundledPluginBuildEntryParams = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
export const NON_PACKAGED_BUNDLED_PLUGIN_DIRS: Set<string>;
|
||||
export function collectBundledPluginBuildEntries(
|
||||
params?: BundledPluginBuildEntryParams,
|
||||
): BundledPluginBuildEntry[];
|
||||
|
||||
@@ -10,6 +10,7 @@ export type BundledPluginBuildEntryParams = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
export const NON_PACKAGED_BUNDLED_PLUGIN_DIRS: Set<string>;
|
||||
export function collectBundledPluginBuildEntries(
|
||||
params?: BundledPluginBuildEntryParams,
|
||||
): BundledPluginBuildEntry[];
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { shouldBuildBundledCluster } from "./optional-bundled-clusters.mjs";
|
||||
|
||||
const TOP_LEVEL_PUBLIC_SURFACE_EXTENSIONS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]);
|
||||
const NON_PACKAGED_BUNDLED_PLUGIN_DIRS = new Set(["qa-channel", "qa-lab", "qa-matrix"]);
|
||||
export const NON_PACKAGED_BUNDLED_PLUGIN_DIRS = new Set(["qa-channel", "qa-lab", "qa-matrix"]);
|
||||
const toPosixPath = (value) => value.replaceAll("\\", "/");
|
||||
|
||||
function readBundledPluginPackageJson(packageJsonPath) {
|
||||
|
||||
@@ -179,7 +179,6 @@
|
||||
"matrix-runtime-surface",
|
||||
"matrix-surface",
|
||||
"matrix-thread-bindings",
|
||||
"qa-runtime",
|
||||
"qa-runner-runtime",
|
||||
"mattermost",
|
||||
"mattermost-policy",
|
||||
|
||||
@@ -46,7 +46,6 @@ const LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER =
|
||||
"Failed to load legacy context engine runtime.";
|
||||
const LEGACY_UPDATE_COMPAT_RUNTIME_SIDECAR_PATHS = [
|
||||
"dist/extensions/qa-channel/runtime-api.js",
|
||||
"dist/extensions/qa-lab/runtime-api.js",
|
||||
] as const;
|
||||
const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = [
|
||||
...BUNDLED_RUNTIME_SIDECAR_PATHS.filter((relativePath) =>
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
import { basename, join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "../src/infra/package-dist-inventory.ts";
|
||||
import {
|
||||
compareReleaseVersions as compareReleaseVersionsBase,
|
||||
resolveNpmDistTagMirrorAuth as resolveNpmDistTagMirrorAuthBase,
|
||||
@@ -55,11 +59,9 @@ export type NpmDistTagMirrorAuth = {
|
||||
};
|
||||
const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
|
||||
const MAX_CALVER_DISTANCE_DAYS = 2;
|
||||
const LEGACY_UPDATE_COMPAT_PACKED_PATHS = [
|
||||
"dist/extensions/qa-channel/runtime-api.js",
|
||||
"dist/extensions/qa-lab/runtime-api.js",
|
||||
] as const;
|
||||
const LEGACY_UPDATE_COMPAT_PACKED_PATHS = ["dist/extensions/qa-channel/runtime-api.js"] as const;
|
||||
const REQUIRED_PACKED_PATHS = [
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
"dist/control-ui/index.html",
|
||||
...LEGACY_UPDATE_COMPAT_PACKED_PATHS,
|
||||
...WORKSPACE_TEMPLATE_PACK_PATHS,
|
||||
@@ -81,7 +83,28 @@ const FORBIDDEN_PACKED_PATH_RULES = [
|
||||
describe: (packedPath: string) =>
|
||||
`npm package must not include private QA lab artifact "${packedPath}".`,
|
||||
},
|
||||
{
|
||||
prefix: "dist/plugin-sdk/extensions/qa-lab/",
|
||||
describe: (packedPath: string) =>
|
||||
`npm package must not include private QA lab type artifact "${packedPath}".`,
|
||||
},
|
||||
{
|
||||
prefix: "dist/qa-runtime-",
|
||||
describe: (packedPath: string) =>
|
||||
`npm package must not include private QA runtime chunk "${packedPath}".`,
|
||||
},
|
||||
{
|
||||
prefix: "qa/",
|
||||
describe: (packedPath: string) =>
|
||||
`npm package must not include private QA suite artifact "${packedPath}".`,
|
||||
},
|
||||
] as const;
|
||||
const FORBIDDEN_PRIVATE_QA_CONTENT_MARKERS = [
|
||||
"//#region extensions/qa-lab/",
|
||||
"qa-lab/cli.js",
|
||||
"qa-lab/runtime-api.js",
|
||||
] as const;
|
||||
const FORBIDDEN_PRIVATE_QA_CONTENT_SCAN_PREFIXES = ["dist/"] as const;
|
||||
const NPM_PACK_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
||||
const skipPackValidationEnv = "OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK";
|
||||
|
||||
@@ -466,6 +489,7 @@ function collectPackedTarballErrors(): string[] {
|
||||
return [
|
||||
...collectControlUiPackErrors(packedPaths),
|
||||
...collectForbiddenPackedPathErrors(packedPaths),
|
||||
...collectForbiddenPackedContentErrors(packedPaths),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -486,7 +510,41 @@ export function collectForbiddenPackedPathErrors(paths: Iterable<string>): strin
|
||||
return errors.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
export function collectForbiddenPackedContentErrors(
|
||||
paths: Iterable<string>,
|
||||
rootDir = process.cwd(),
|
||||
): string[] {
|
||||
const textPathPattern = /\.(?:[cm]?js|d\.ts|json|md|mjs|cjs)$/u;
|
||||
const errors: string[] = [];
|
||||
for (const packedPath of paths) {
|
||||
if (
|
||||
!FORBIDDEN_PRIVATE_QA_CONTENT_SCAN_PREFIXES.some((prefix) => packedPath.startsWith(prefix))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!textPathPattern.test(packedPath)) {
|
||||
continue;
|
||||
}
|
||||
let content: string;
|
||||
try {
|
||||
content = readFileSync(pathToFileURL(join(rootDir, packedPath)), "utf8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const matchedMarker = FORBIDDEN_PRIVATE_QA_CONTENT_MARKERS.find((marker) =>
|
||||
content.includes(marker),
|
||||
);
|
||||
if (!matchedMarker) {
|
||||
continue;
|
||||
}
|
||||
errors.push(
|
||||
`npm package must not include private QA lab marker "${matchedMarker}" in "${packedPath}".`,
|
||||
);
|
||||
}
|
||||
return errors.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const pkg = loadPackageJson();
|
||||
const now = new Date();
|
||||
const skipPackValidation = shouldSkipPackedTarballValidation();
|
||||
@@ -498,6 +556,9 @@ function main(): number {
|
||||
releaseMainRef: process.env.RELEASE_MAIN_REF,
|
||||
now,
|
||||
});
|
||||
if (!skipPackValidation) {
|
||||
await writePackageDistInventory(process.cwd());
|
||||
}
|
||||
const tarballErrors = skipPackValidation ? [] : collectPackedTarballErrors();
|
||||
const errors = [...metadataErrors, ...tagErrors, ...tarballErrors];
|
||||
|
||||
@@ -519,5 +580,5 @@ function main(): number {
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
process.exit(main());
|
||||
process.exit(await main());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
closeSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
@@ -33,6 +34,14 @@ const DEFAULT_EXTENSIONS_DIR = join(__dirname, "..", "dist", "extensions");
|
||||
const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
|
||||
const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
|
||||
const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json";
|
||||
const LEGACY_UPDATE_COMPAT_SIDECARS = [
|
||||
{
|
||||
path: "dist/extensions/qa-lab/runtime-api.js",
|
||||
removedPrefix: "dist/extensions/qa-lab/",
|
||||
content:
|
||||
"// Compatibility stub for older OpenClaw updaters. QA Lab is not packaged.\nexport {};\n",
|
||||
},
|
||||
];
|
||||
const BAILEYS_MEDIA_FILE = join(
|
||||
"node_modules",
|
||||
"@whiskeysockets",
|
||||
@@ -296,6 +305,30 @@ export function pruneInstalledPackageDist(params = {}) {
|
||||
return removed;
|
||||
}
|
||||
|
||||
export function restoreLegacyUpdaterCompatSidecars(params = {}) {
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const writeFile = params.writeFileSync ?? writeFileSync;
|
||||
const makeDirectory = params.mkdirSync ?? mkdirSync;
|
||||
const log = params.log ?? console;
|
||||
const restored = [];
|
||||
|
||||
for (const sidecar of LEGACY_UPDATE_COMPAT_SIDECARS) {
|
||||
// Older npm updater builds verify this exact sidecar after npm has already
|
||||
// replaced the package. npm may remove stale QA Lab files before this
|
||||
// postinstall hook runs, so this must be generated independently of prune
|
||||
// results. The tarball and dist inventory still omit QA Lab.
|
||||
const sidecarPath = join(packageRoot, sidecar.path);
|
||||
makeDirectory(dirname(sidecarPath), { recursive: true });
|
||||
writeFile(sidecarPath, sidecar.content, "utf8");
|
||||
restored.push(sidecar.path);
|
||||
}
|
||||
|
||||
if (restored.length > 0) {
|
||||
log.log(`[postinstall] restored legacy updater compat sidecars: ${restored.join(", ")}`);
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
function dependencySentinelPath(depName) {
|
||||
return join("node_modules", ...depName.split("/"), "package.json");
|
||||
}
|
||||
@@ -650,7 +683,7 @@ export function runBundledPluginPostinstall(params = {}) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
pruneInstalledPackageDist({
|
||||
const prunedDistFiles = pruneInstalledPackageDist({
|
||||
packageRoot,
|
||||
existsSync: pathExists,
|
||||
readFileSync: params.readFileSync,
|
||||
@@ -658,6 +691,13 @@ export function runBundledPluginPostinstall(params = {}) {
|
||||
rmSync: params.rmSync,
|
||||
log,
|
||||
});
|
||||
restoreLegacyUpdaterCompatSidecars({
|
||||
packageRoot,
|
||||
removedFiles: prunedDistFiles,
|
||||
mkdirSync: params.mkdirSync,
|
||||
writeFileSync: params.writeFileSync,
|
||||
log,
|
||||
});
|
||||
if (
|
||||
!shouldRunBundledPluginPostinstall({
|
||||
env,
|
||||
|
||||
@@ -5,6 +5,10 @@ import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "../src/infra/package-dist-inventory.ts";
|
||||
import {
|
||||
collectBundledExtensionManifestErrors,
|
||||
type BundledExtension,
|
||||
@@ -38,13 +42,13 @@ type PackFile = { path: string };
|
||||
type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number };
|
||||
|
||||
const requiredPathGroups = [
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
["dist/index.js", "dist/index.mjs"],
|
||||
["dist/entry.js", "dist/entry.mjs"],
|
||||
...listPluginSdkDistArtifacts(),
|
||||
...listBundledPluginPackArtifacts(),
|
||||
...listStaticExtensionAssetOutputs(),
|
||||
...WORKSPACE_TEMPLATE_PACK_PATHS,
|
||||
...listRequiredQaScenarioPackPaths(),
|
||||
"scripts/npm-runner.mjs",
|
||||
"scripts/preinstall-package-manager-warning.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
@@ -57,21 +61,27 @@ const requiredPathGroups = [
|
||||
const forbiddenPrefixes = [
|
||||
"dist-runtime/",
|
||||
"dist/OpenClaw.app/",
|
||||
"dist/extensions/qa-lab/",
|
||||
"dist/plugin-sdk/extensions/qa-lab/",
|
||||
"dist/plugin-sdk/qa-lab.",
|
||||
"dist/plugin-sdk/qa-runtime.",
|
||||
"dist/plugin-sdk/src/plugin-sdk/qa-lab.d.ts",
|
||||
"dist/plugin-sdk/src/plugin-sdk/qa-runtime.d.ts",
|
||||
"dist/qa-runtime-",
|
||||
"dist/plugin-sdk/.tsbuildinfo",
|
||||
"docs/.generated/",
|
||||
"qa/",
|
||||
];
|
||||
const forbiddenPrivateQaContentMarkers = [
|
||||
"//#region extensions/qa-lab/",
|
||||
"qa-lab/cli.js",
|
||||
"qa-lab/runtime-api.js",
|
||||
] as const;
|
||||
const forbiddenPrivateQaContentScanPrefixes = ["dist/"] as const;
|
||||
const appcastPath = resolve("appcast.xml");
|
||||
const laneBuildMin = 1_000_000_000;
|
||||
const laneFloorAdoptionDateKey = 20260227;
|
||||
|
||||
export function listRequiredQaScenarioPackPaths(): string[] {
|
||||
const scenariosDir = resolve("qa/scenarios");
|
||||
return readdirSync(scenariosDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
||||
.map((entry) => `qa/scenarios/${entry.name}`)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function collectBundledExtensions(): BundledExtension[] {
|
||||
const extensionsDir = resolve("extensions");
|
||||
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
|
||||
@@ -269,6 +279,30 @@ export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function collectForbiddenPackContentPaths(
|
||||
paths: Iterable<string>,
|
||||
rootDir = process.cwd(),
|
||||
): string[] {
|
||||
const textPathPattern = /\.(?:[cm]?js|d\.ts|json|md|mjs|cjs)$/u;
|
||||
return [...paths]
|
||||
.filter((packedPath) => {
|
||||
if (!forbiddenPrivateQaContentScanPrefixes.some((prefix) => packedPath.startsWith(prefix))) {
|
||||
return false;
|
||||
}
|
||||
if (!textPathPattern.test(packedPath)) {
|
||||
return false;
|
||||
}
|
||||
let content: string;
|
||||
try {
|
||||
content = readFileSync(resolve(rootDir, packedPath), "utf8");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return forbiddenPrivateQaContentMarkers.some((marker) => content.includes(marker));
|
||||
})
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export { collectPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs";
|
||||
|
||||
function extractTag(item: string, tag: string): string | null {
|
||||
@@ -430,6 +464,7 @@ async function main() {
|
||||
checkAppcastSparkleVersions();
|
||||
await checkPluginSdkExports();
|
||||
checkBundledExtensionMetadata();
|
||||
await writePackageDistInventory(process.cwd());
|
||||
|
||||
const results = runPackDry();
|
||||
const files = results.flatMap((entry) => entry.files ?? []);
|
||||
@@ -444,9 +479,15 @@ async function main() {
|
||||
})
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
const forbidden = collectForbiddenPackPaths(paths);
|
||||
const forbiddenContent = collectForbiddenPackContentPaths(paths);
|
||||
const sizeErrors = collectNpmPackUnpackedSizeErrors(results);
|
||||
|
||||
if (missing.length > 0 || forbidden.length > 0 || sizeErrors.length > 0) {
|
||||
if (
|
||||
missing.length > 0 ||
|
||||
forbidden.length > 0 ||
|
||||
forbiddenContent.length > 0 ||
|
||||
sizeErrors.length > 0
|
||||
) {
|
||||
if (missing.length > 0) {
|
||||
console.error("release-check: missing files in npm pack:");
|
||||
for (const path of missing) {
|
||||
@@ -471,6 +512,12 @@ async function main() {
|
||||
console.error(` - ${path}`);
|
||||
}
|
||||
}
|
||||
if (forbiddenContent.length > 0) {
|
||||
console.error("release-check: forbidden private QA markers in npm pack:");
|
||||
for (const path of forbiddenContent) {
|
||||
console.error(` - ${path}`);
|
||||
}
|
||||
}
|
||||
if (sizeErrors.length > 0) {
|
||||
console.error("release-check: npm pack unpacked size budget exceeded:");
|
||||
for (const error of sizeErrors) {
|
||||
|
||||
@@ -208,6 +208,13 @@ export const resolveBuildRequirement = (deps) => {
|
||||
if (deps.env.OPENCLAW_FORCE_BUILD === "1") {
|
||||
return { shouldBuild: true, reason: "force_build" };
|
||||
}
|
||||
if (
|
||||
deps.env.OPENCLAW_BUILD_PRIVATE_QA === "1" &&
|
||||
deps.privateQaDistEntry &&
|
||||
statMtime(deps.privateQaDistEntry, deps.fs) == null
|
||||
) {
|
||||
return { shouldBuild: true, reason: "missing_private_qa_dist" };
|
||||
}
|
||||
const stamp = readBuildStamp(deps);
|
||||
if (stamp.mtime == null) {
|
||||
return { shouldBuild: true, reason: "missing_build_stamp" };
|
||||
@@ -255,6 +262,7 @@ const BUILD_REASON_LABELS = {
|
||||
git_head_changed: "git head changed",
|
||||
dirty_watched_tree: "dirty watched source tree",
|
||||
source_mtime_newer: "source mtime newer than build stamp",
|
||||
missing_private_qa_dist: "private QA dist entry missing",
|
||||
clean: "clean",
|
||||
};
|
||||
|
||||
@@ -389,6 +397,11 @@ export async function runNodeMain(params = {}) {
|
||||
path: path.join(deps.cwd, sourceRoot),
|
||||
}));
|
||||
deps.configFiles = runNodeConfigFiles.map((filePath) => path.join(deps.cwd, filePath));
|
||||
deps.privateQaDistEntry = path.join(deps.distRoot, "extensions", "qa-lab", "cli.js");
|
||||
if (deps.args[0] === "qa") {
|
||||
deps.env.OPENCLAW_BUILD_PRIVATE_QA = "1";
|
||||
deps.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI = "1";
|
||||
}
|
||||
|
||||
const buildRequirement = resolveBuildRequirement(deps);
|
||||
if (!buildRequirement.shouldBuild) {
|
||||
|
||||
@@ -228,6 +228,7 @@ prepare_update_tarball() {
|
||||
UPDATE_EXPECT_VERSION="$(
|
||||
node -p 'JSON.parse(require("node:fs").readFileSync("package.json", "utf8")).version'
|
||||
)"
|
||||
node --import tsx scripts/write-package-dist-inventory.ts
|
||||
quiet_npm pack --ignore-scripts --json --pack-destination "$UPDATE_DIR" >"$pack_json_file"
|
||||
fi
|
||||
UPDATE_TGZ_FILE="$(
|
||||
|
||||
12
scripts/write-package-dist-inventory.ts
Normal file
12
scripts/write-package-dist-inventory.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { writePackageDistInventory } from "../src/infra/package-dist-inventory.ts";
|
||||
|
||||
export async function writeCurrentPackageDistInventory(): Promise<void> {
|
||||
await writePackageDistInventory(process.cwd());
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
await writeCurrentPackageDistInventory();
|
||||
}
|
||||
8
src/cli/program/private-qa-cli.ts
Normal file
8
src/cli/program/private-qa-cli.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function isPrivateQaCliEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return env.OPENCLAW_ENABLE_PRIVATE_QA_CLI === "1";
|
||||
}
|
||||
|
||||
export function loadPrivateQaCliModule(): Promise<Record<string, unknown>> {
|
||||
const specifier = ["../../plugin-sdk/", "qa", "-lab.js"].join("");
|
||||
return import(specifier) as Promise<Record<string, unknown>>;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
defineImportedProgramCommandGroupSpecs,
|
||||
type CommandGroupDescriptorSpec,
|
||||
} from "./command-group-descriptors.js";
|
||||
import { loadPrivateQaCliModule } from "./private-qa-cli.js";
|
||||
import {
|
||||
registerCommandGroupByName,
|
||||
registerCommandGroups,
|
||||
@@ -131,7 +132,7 @@ const entrySpecs: readonly CommandGroupDescriptorSpec<SubCliRegistrar>[] = [
|
||||
},
|
||||
{
|
||||
commandNames: ["qa"],
|
||||
loadModule: () => import("../../plugin-sdk/qa-lab.js"),
|
||||
loadModule: loadPrivateQaCliModule,
|
||||
exportName: "registerQaLabCli",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,8 +19,7 @@ const { nodesAction, registerNodesCli } = vi.hoisted(() => {
|
||||
return { nodesAction: action, registerNodesCli: register };
|
||||
});
|
||||
|
||||
const { isQaLabCliAvailable, registerQaLabCli } = vi.hoisted(() => ({
|
||||
isQaLabCliAvailable: vi.fn(() => true),
|
||||
const { registerQaLabCli } = vi.hoisted(() => ({
|
||||
registerQaLabCli: vi.fn((program: Command) => {
|
||||
const qa = program.command("qa");
|
||||
qa.command("run").action(() => undefined);
|
||||
@@ -38,11 +37,12 @@ const { inferAction, registerCapabilityCli } = vi.hoisted(() => {
|
||||
vi.mock("../acp-cli.js", () => ({ registerAcpCli }));
|
||||
vi.mock("../nodes-cli.js", () => ({ registerNodesCli }));
|
||||
vi.mock("../capability-cli.js", () => ({ registerCapabilityCli }));
|
||||
vi.mock("../../plugin-sdk/qa-lab.js", () => ({ isQaLabCliAvailable, registerQaLabCli }));
|
||||
vi.mock("../../plugin-sdk/qa-lab.js", () => ({ registerQaLabCli }));
|
||||
|
||||
describe("registerSubCliCommands", () => {
|
||||
const originalArgv = process.argv;
|
||||
const originalDisableLazySubcommands = process.env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS;
|
||||
const originalEnablePrivateQaCli = process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI;
|
||||
|
||||
const createRegisteredProgram = (argv: string[], name?: string) => {
|
||||
process.argv = argv;
|
||||
@@ -60,11 +60,11 @@ describe("registerSubCliCommands", () => {
|
||||
} else {
|
||||
process.env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS = originalDisableLazySubcommands;
|
||||
}
|
||||
process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI = "1";
|
||||
registerAcpCli.mockClear();
|
||||
acpAction.mockClear();
|
||||
registerNodesCli.mockClear();
|
||||
nodesAction.mockClear();
|
||||
isQaLabCliAvailable.mockReset().mockReturnValue(true);
|
||||
registerQaLabCli.mockClear();
|
||||
registerCapabilityCli.mockClear();
|
||||
inferAction.mockClear();
|
||||
@@ -77,6 +77,11 @@ describe("registerSubCliCommands", () => {
|
||||
} else {
|
||||
process.env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS = originalDisableLazySubcommands;
|
||||
}
|
||||
if (originalEnablePrivateQaCli === undefined) {
|
||||
delete process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI;
|
||||
} else {
|
||||
process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI = originalEnablePrivateQaCli;
|
||||
}
|
||||
});
|
||||
|
||||
it("registers the primary placeholder plus completion and dispatches", async () => {
|
||||
@@ -101,8 +106,8 @@ describe("registerSubCliCommands", () => {
|
||||
expect(registerAcpCli).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("omits the qa placeholder when the private qa bundle is unavailable", () => {
|
||||
isQaLabCliAvailable.mockReturnValue(false);
|
||||
it("omits the qa placeholder when the private qa cli is disabled", () => {
|
||||
delete process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI;
|
||||
|
||||
const program = createRegisteredProgram(["node", "openclaw"]);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isQaLabCliAvailable } from "../../plugin-sdk/qa-lab.js";
|
||||
import { defineCommandDescriptorCatalog } from "./command-descriptor-utils.js";
|
||||
import type { NamedCommandDescriptor } from "./command-group-descriptors.js";
|
||||
import { isPrivateQaCliEnabled } from "./private-qa-cli.js";
|
||||
|
||||
export type SubCliDescriptor = NamedCommandDescriptor;
|
||||
|
||||
@@ -164,7 +164,7 @@ export const SUB_CLI_DESCRIPTORS = subCliCommandCatalog.descriptors;
|
||||
|
||||
export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
|
||||
const descriptors = subCliCommandCatalog.getDescriptors();
|
||||
if (isQaLabCliAvailable()) {
|
||||
if (isPrivateQaCliEnabled()) {
|
||||
return descriptors;
|
||||
}
|
||||
return descriptors.filter((descriptor) => descriptor.name !== "qa");
|
||||
@@ -172,7 +172,7 @@ export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
|
||||
|
||||
export function getSubCliCommandsWithSubcommands(): string[] {
|
||||
const commands = subCliCommandCatalog.getCommandsWithSubcommands();
|
||||
if (isQaLabCliAvailable()) {
|
||||
if (isPrivateQaCliEnabled()) {
|
||||
return commands;
|
||||
}
|
||||
return commands.filter((command) => command !== "qa");
|
||||
|
||||
@@ -4,13 +4,13 @@ export const NPM_UPDATE_COMPAT_SIDECARS = [
|
||||
content:
|
||||
"// Compatibility stub for older OpenClaw updaters. The QA channel implementation is not packaged.\nexport {};\n",
|
||||
},
|
||||
{
|
||||
path: "dist/extensions/qa-lab/runtime-api.js",
|
||||
content:
|
||||
"// Compatibility stub for older OpenClaw updaters. The QA lab implementation is not packaged.\nexport {};\n",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const NPM_UPDATE_COMPAT_SIDECAR_PATHS = new Set<string>(
|
||||
NPM_UPDATE_COMPAT_SIDECARS.map((entry) => entry.path),
|
||||
);
|
||||
|
||||
export const NPM_UPDATE_OMITTED_BUNDLED_PLUGIN_ROOTS = new Set<string>([
|
||||
"dist/extensions/qa-lab",
|
||||
"dist/extensions/qa-matrix",
|
||||
]);
|
||||
|
||||
@@ -52,12 +52,41 @@ describe("package dist inventory", () => {
|
||||
"qa-matrix",
|
||||
"index.js",
|
||||
);
|
||||
const omittedQaLabPluginSdk = path.join(packageRoot, "dist", "plugin-sdk", "qa-lab.js");
|
||||
const omittedQaLabTypes = path.join(
|
||||
packageRoot,
|
||||
"dist",
|
||||
"plugin-sdk",
|
||||
"extensions",
|
||||
"qa-lab",
|
||||
"cli.d.ts",
|
||||
);
|
||||
const omittedQaRuntimeChunk = path.join(packageRoot, "dist", "qa-runtime-B9LDtssJ.js");
|
||||
const omittedExtensionNodeModuleSymlink = path.join(
|
||||
packageRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
"discord",
|
||||
"node_modules",
|
||||
".bin",
|
||||
"color-support",
|
||||
);
|
||||
const omittedMap = path.join(packageRoot, "dist", "feature.runtime.js.map");
|
||||
await fs.mkdir(path.dirname(packagedQaRuntime), { recursive: true });
|
||||
await fs.mkdir(path.dirname(omittedQaMatrixChunk), { recursive: true });
|
||||
await fs.mkdir(path.dirname(omittedQaLabTypes), { recursive: true });
|
||||
await fs.mkdir(path.dirname(omittedExtensionNodeModuleSymlink), { recursive: true });
|
||||
await fs.writeFile(path.join(packageRoot, "color-support.js"), "export {};\n", "utf8");
|
||||
await fs.writeFile(packagedQaRuntime, "export {};\n", "utf8");
|
||||
await fs.writeFile(omittedQaChunk, "export {};\n", "utf8");
|
||||
await fs.writeFile(omittedQaMatrixChunk, "export {};\n", "utf8");
|
||||
await fs.writeFile(omittedQaLabPluginSdk, "export {};\n", "utf8");
|
||||
await fs.writeFile(omittedQaLabTypes, "export {};\n", "utf8");
|
||||
await fs.writeFile(omittedQaRuntimeChunk, "export {};\n", "utf8");
|
||||
await fs.symlink(
|
||||
path.join(packageRoot, "color-support.js"),
|
||||
omittedExtensionNodeModuleSymlink,
|
||||
);
|
||||
await fs.writeFile(omittedMap, "{}", "utf8");
|
||||
|
||||
await expect(writePackageDistInventory(packageRoot)).resolves.toEqual([
|
||||
|
||||
@@ -2,15 +2,28 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export const PACKAGE_DIST_INVENTORY_RELATIVE_PATH = "dist/postinstall-inventory.json";
|
||||
const PACKAGED_QA_RUNTIME_PATHS = new Set([
|
||||
"dist/extensions/qa-channel/runtime-api.js",
|
||||
"dist/extensions/qa-lab/runtime-api.js",
|
||||
]);
|
||||
const PACKAGED_QA_RUNTIME_PATHS = new Set(["dist/extensions/qa-channel/runtime-api.js"]);
|
||||
const OMITTED_QA_EXTENSION_PREFIXES = [
|
||||
"dist/extensions/qa-channel/",
|
||||
"dist/extensions/qa-lab/",
|
||||
"dist/extensions/qa-matrix/",
|
||||
];
|
||||
const OMITTED_PRIVATE_QA_PLUGIN_SDK_PREFIXES = ["dist/plugin-sdk/extensions/qa-lab/"];
|
||||
const OMITTED_PRIVATE_QA_PLUGIN_SDK_FILES = new Set([
|
||||
"dist/plugin-sdk/qa-lab.d.ts",
|
||||
"dist/plugin-sdk/qa-lab.js",
|
||||
"dist/plugin-sdk/qa-runtime.d.ts",
|
||||
"dist/plugin-sdk/qa-runtime.js",
|
||||
"dist/plugin-sdk/src/plugin-sdk/qa-lab.d.ts",
|
||||
"dist/plugin-sdk/src/plugin-sdk/qa-runtime.d.ts",
|
||||
]);
|
||||
const OMITTED_PRIVATE_QA_DIST_PREFIXES = ["dist/qa-runtime-"];
|
||||
const OMITTED_DIST_SUBTREE_PATTERNS = [
|
||||
/^dist\/extensions\/[^/]+\/node_modules(?:\/|$)/u,
|
||||
/^dist\/extensions\/qa-lab(?:\/|$)/u,
|
||||
/^dist\/extensions\/qa-matrix(?:\/|$)/u,
|
||||
/^dist\/plugin-sdk\/extensions\/qa-lab(?:\/|$)/u,
|
||||
] as const;
|
||||
|
||||
function normalizeRelativePath(value: string): string {
|
||||
return value.replace(/\\/g, "/");
|
||||
@@ -29,12 +42,28 @@ function isPackagedDistPath(relativePath: string): boolean {
|
||||
if (relativePath === "dist/plugin-sdk/.tsbuildinfo") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
OMITTED_PRIVATE_QA_PLUGIN_SDK_PREFIXES.some((prefix) => relativePath.startsWith(prefix)) ||
|
||||
OMITTED_PRIVATE_QA_PLUGIN_SDK_FILES.has(relativePath) ||
|
||||
OMITTED_PRIVATE_QA_DIST_PREFIXES.some((prefix) => relativePath.startsWith(prefix))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (OMITTED_QA_EXTENSION_PREFIXES.some((prefix) => relativePath.startsWith(prefix))) {
|
||||
return PACKAGED_QA_RUNTIME_PATHS.has(relativePath);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isOmittedDistSubtree(relativePath: string): boolean {
|
||||
return OMITTED_DIST_SUBTREE_PATTERNS.some((pattern) => pattern.test(relativePath));
|
||||
}
|
||||
|
||||
async function collectRelativeFiles(rootDir: string, baseDir: string): Promise<string[]> {
|
||||
const rootRelativePath = normalizeRelativePath(path.relative(baseDir, rootDir));
|
||||
if (rootRelativePath && isOmittedDistSubtree(rootRelativePath)) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const rootStats = await fs.lstat(rootDir);
|
||||
if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) {
|
||||
|
||||
@@ -533,4 +533,38 @@ describe("update global helpers", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores stale metadata for non-packaged private QA plugins during inventory verify", async () => {
|
||||
await withTempDir(
|
||||
{ prefix: "openclaw-update-global-stale-private-qa-" },
|
||||
async (packageRoot) => {
|
||||
await fs.writeFile(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.4.15" }),
|
||||
"utf-8",
|
||||
);
|
||||
for (const relativePath of NPM_UPDATE_COMPAT_SIDECAR_PATHS) {
|
||||
const absolutePath = path.join(packageRoot, relativePath);
|
||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await fs.writeFile(absolutePath, "export {};\n", "utf-8");
|
||||
}
|
||||
const staleQaLabPackageJson = path.join(
|
||||
packageRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
"qa-lab",
|
||||
"package.json",
|
||||
);
|
||||
await fs.mkdir(path.dirname(staleQaLabPackageJson), { recursive: true });
|
||||
await fs.writeFile(
|
||||
staleQaLabPackageJson,
|
||||
JSON.stringify({ name: "@openclaw/qa-lab" }),
|
||||
"utf-8",
|
||||
);
|
||||
await writePackageDistInventory(packageRoot);
|
||||
|
||||
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,10 @@ import path from "node:path";
|
||||
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/runtime-sidecar-paths.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { pathExists } from "../utils.js";
|
||||
import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "./npm-update-compat-sidecars.js";
|
||||
import {
|
||||
NPM_UPDATE_COMPAT_SIDECAR_PATHS,
|
||||
NPM_UPDATE_OMITTED_BUNDLED_PLUGIN_ROOTS,
|
||||
} from "./npm-update-compat-sidecars.js";
|
||||
import {
|
||||
collectPackageDistInventory,
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
@@ -202,6 +205,9 @@ async function collectCriticalInstalledPackageDistPaths(packageRoot: string): Pr
|
||||
if (pluginRoot === null) {
|
||||
return;
|
||||
}
|
||||
if (NPM_UPDATE_OMITTED_BUNDLED_PLUGIN_ROOTS.has(pluginRoot)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(await pathExists(path.join(packageRoot, pluginRoot, "package.json"))) ||
|
||||
(await pathExists(path.join(packageRoot, pluginRoot, "openclaw.plugin.json")))
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// Manual facade. Keep loader boundary explicit.
|
||||
type FacadeModule = typeof import("@openclaw/qa-lab/cli.js");
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-loader.js";
|
||||
|
||||
type FacadeModule = {
|
||||
isQaLabCliAvailable: () => boolean;
|
||||
registerQaLabCli: (program: unknown) => void;
|
||||
};
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "qa-lab",
|
||||
|
||||
@@ -318,6 +318,30 @@ describe("copyBundledPluginMetadata", () => {
|
||||
expect(fs.existsSync(staleDistDir)).toBe(false);
|
||||
});
|
||||
|
||||
it("removes non-packaged private QA plugin metadata unless private QA build is enabled", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-private-qa-metadata-");
|
||||
createPlugin(repoRoot, {
|
||||
id: "qa-lab",
|
||||
packageName: "@openclaw/qa-lab",
|
||||
packageOpenClaw: { extensions: ["./index.ts"] },
|
||||
});
|
||||
const staleDistDir = path.join(repoRoot, "dist", "extensions", "qa-lab");
|
||||
fs.mkdirSync(staleDistDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staleDistDir, "runtime-api.js"), "export {};\n", "utf8");
|
||||
|
||||
copyBundledPluginMetadataWithEnv({ repoRoot, env: {} });
|
||||
|
||||
expect(fs.existsSync(staleDistDir)).toBe(false);
|
||||
|
||||
copyBundledPluginMetadataWithEnv({
|
||||
repoRoot,
|
||||
env: { OPENCLAW_BUILD_PRIVATE_QA: "1" } as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(fs.existsSync(path.join(staleDistDir, "openclaw.plugin.json"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(staleDistDir, "package.json"))).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "skips metadata for optional bundled clusters only when explicitly disabled",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { defineConfig, type UserConfig } from "tsdown";
|
||||
import {
|
||||
collectBundledPluginBuildEntries,
|
||||
listBundledPluginRuntimeDependencies,
|
||||
NON_PACKAGED_BUNDLED_PLUGIN_DIRS,
|
||||
} from "./scripts/lib/bundled-plugin-build-entries.mjs";
|
||||
import { buildPluginSdkEntrySources } from "./scripts/lib/plugin-sdk-entries.mjs";
|
||||
|
||||
@@ -92,6 +93,7 @@ function nodeBuildConfig(config: UserConfig): UserConfig {
|
||||
|
||||
const bundledPluginBuildEntries = collectBundledPluginBuildEntries();
|
||||
const bundledPluginRuntimeDependencies = listBundledPluginRuntimeDependencies();
|
||||
const shouldBuildPrivateQaEntries = process.env.OPENCLAW_BUILD_PRIVATE_QA === "1";
|
||||
|
||||
function buildBundledHookEntries(): Record<string, string> {
|
||||
const hooksRoot = path.join(process.cwd(), "src", "hooks", "bundled");
|
||||
@@ -235,7 +237,9 @@ const stagedBundledPluginBuildEntries = bundledPluginBuildEntries.filter(({ pack
|
||||
shouldStageBundledPluginRuntimeDependencies(packageJson),
|
||||
);
|
||||
const rootBundledPluginBuildEntries = bundledPluginBuildEntries.filter(
|
||||
({ packageJson }) => !shouldStageBundledPluginRuntimeDependencies(packageJson),
|
||||
({ id, packageJson }) =>
|
||||
!shouldStageBundledPluginRuntimeDependencies(packageJson) &&
|
||||
(shouldBuildPrivateQaEntries || !NON_PACKAGED_BUNDLED_PLUGIN_DIRS.has(id)),
|
||||
);
|
||||
|
||||
function buildUnifiedDistEntries(): Record<string, string> {
|
||||
@@ -249,6 +253,12 @@ function buildUnifiedDistEntries(): Record<string, string> {
|
||||
source,
|
||||
]),
|
||||
),
|
||||
...(shouldBuildPrivateQaEntries
|
||||
? {
|
||||
"plugin-sdk/qa-lab": "src/plugin-sdk/qa-lab.ts",
|
||||
"plugin-sdk/qa-runtime": "src/plugin-sdk/qa-runtime.ts",
|
||||
}
|
||||
: {}),
|
||||
...listBundledPluginEntrySources(rootBundledPluginBuildEntries),
|
||||
...bundledHookEntries,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user