mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
fix(release): keep private QA bundles out of npm pack
This commit is contained in:
@@ -1 +1 @@
|
||||
export { registerQaLabCli } from "./src/cli.js";
|
||||
export { isQaLabCliAvailable, registerQaLabCli } from "./src/cli.js";
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
||||
import { collectString } from "./cli-options.js";
|
||||
import { LIVE_TRANSPORT_QA_CLI_REGISTRATIONS } from "./live-transports/cli.js";
|
||||
import type { QaProviderModeInput } from "./run-config.js";
|
||||
import { hasQaScenarioPack } from "./scenario-catalog.js";
|
||||
|
||||
type QaLabCliRuntime = typeof import("./cli.runtime.js");
|
||||
|
||||
@@ -125,6 +126,10 @@ async function runQaMockOpenAi(opts: { host?: string; port?: number }) {
|
||||
await runtime.runQaMockOpenAiCommand(opts);
|
||||
}
|
||||
|
||||
export function isQaLabCliAvailable(): boolean {
|
||||
return hasQaScenarioPack();
|
||||
}
|
||||
|
||||
export function registerQaLabCli(program: Command) {
|
||||
const qa = program
|
||||
.command("qa")
|
||||
|
||||
@@ -181,6 +181,10 @@ function resolveRepoPath(relativePath: string, kind: "file" | "directory" = "fil
|
||||
return null;
|
||||
}
|
||||
|
||||
export function hasQaScenarioPack(): boolean {
|
||||
return resolveRepoPath(QA_SCENARIO_PACK_INDEX_PATH, "file") !== null;
|
||||
}
|
||||
|
||||
function readTextFile(relativePath: string): string {
|
||||
const resolved = resolveRepoPath(relativePath, "file");
|
||||
if (!resolved) {
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"dist/",
|
||||
"!dist/**/*.map",
|
||||
"!dist/plugin-sdk/.tsbuildinfo",
|
||||
"!dist/extensions/qa-channel/**",
|
||||
"!dist/extensions/qa-lab/**",
|
||||
"docs/",
|
||||
"!docs/.generated/**",
|
||||
"!docs/.i18n/zh-CN.tm.jsonl",
|
||||
|
||||
@@ -56,7 +56,23 @@ const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
|
||||
const MAX_CALVER_DISTANCE_DAYS = 2;
|
||||
const REQUIRED_PACKED_PATHS = ["dist/control-ui/index.html"];
|
||||
const CONTROL_UI_ASSET_PREFIX = "dist/control-ui/assets/";
|
||||
const FORBIDDEN_PACKED_PATH_PREFIXES = ["docs/.generated/"] as const;
|
||||
const FORBIDDEN_PACKED_PATH_RULES = [
|
||||
{
|
||||
prefix: "docs/.generated/",
|
||||
describe: (packedPath: string) =>
|
||||
`npm package must not include generated docs artifact "${packedPath}".`,
|
||||
},
|
||||
{
|
||||
prefix: "dist/extensions/qa-channel/",
|
||||
describe: (packedPath: string) =>
|
||||
`npm package must not include private QA channel artifact "${packedPath}".`,
|
||||
},
|
||||
{
|
||||
prefix: "dist/extensions/qa-lab/",
|
||||
describe: (packedPath: string) =>
|
||||
`npm package must not include private QA lab artifact "${packedPath}".`,
|
||||
},
|
||||
] as const;
|
||||
const NPM_PACK_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
||||
const skipPackValidationEnv = "OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK";
|
||||
|
||||
@@ -447,10 +463,13 @@ function collectPackedTarballErrors(): string[] {
|
||||
export function collectForbiddenPackedPathErrors(paths: Iterable<string>): string[] {
|
||||
const errors: string[] = [];
|
||||
for (const packedPath of paths) {
|
||||
if (!FORBIDDEN_PACKED_PATH_PREFIXES.some((prefix) => packedPath.startsWith(prefix))) {
|
||||
const matchedRule = FORBIDDEN_PACKED_PATH_RULES.find((rule) =>
|
||||
packedPath.startsWith(rule.prefix),
|
||||
);
|
||||
if (!matchedRule) {
|
||||
continue;
|
||||
}
|
||||
errors.push(`npm package must not include generated docs artifact "${packedPath}".`);
|
||||
errors.push(matchedRule.describe(packedPath));
|
||||
}
|
||||
return errors.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
@@ -216,7 +216,13 @@ const entrySpecs: readonly CommandGroupDescriptorSpec<SubCliRegistrar>[] = [
|
||||
];
|
||||
|
||||
function resolveSubCliCommandGroups(): CommandGroupEntry[] {
|
||||
return buildCommandGroupEntries(getSubCliEntryDescriptors(), entrySpecs, (register) => register);
|
||||
const descriptors = getSubCliEntryDescriptors();
|
||||
const descriptorNames = new Set(descriptors.map((descriptor) => descriptor.name));
|
||||
return buildCommandGroupEntries(
|
||||
descriptors,
|
||||
entrySpecs.filter((spec) => spec.commandNames.every((name) => descriptorNames.has(name))),
|
||||
(register) => register,
|
||||
);
|
||||
}
|
||||
|
||||
export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
|
||||
|
||||
@@ -19,8 +19,9 @@ const { nodesAction, registerNodesCli } = vi.hoisted(() => {
|
||||
return { nodesAction: action, registerNodesCli: register };
|
||||
});
|
||||
|
||||
const { registerQaCli } = vi.hoisted(() => ({
|
||||
registerQaCli: vi.fn((program: Command) => {
|
||||
const { isQaLabCliAvailable, registerQaLabCli } = vi.hoisted(() => ({
|
||||
isQaLabCliAvailable: vi.fn(() => true),
|
||||
registerQaLabCli: vi.fn((program: Command) => {
|
||||
const qa = program.command("qa");
|
||||
qa.command("run").action(() => undefined);
|
||||
}),
|
||||
@@ -36,8 +37,8 @@ const { inferAction, registerCapabilityCli } = vi.hoisted(() => {
|
||||
|
||||
vi.mock("../acp-cli.js", () => ({ registerAcpCli }));
|
||||
vi.mock("../nodes-cli.js", () => ({ registerNodesCli }));
|
||||
vi.mock("../qa-cli.js", () => ({ registerQaCli }));
|
||||
vi.mock("../capability-cli.js", () => ({ registerCapabilityCli }));
|
||||
vi.mock("../../plugin-sdk/qa-lab.js", () => ({ isQaLabCliAvailable, registerQaLabCli }));
|
||||
|
||||
describe("registerSubCliCommands", () => {
|
||||
const originalArgv = process.argv;
|
||||
@@ -63,6 +64,8 @@ describe("registerSubCliCommands", () => {
|
||||
acpAction.mockClear();
|
||||
registerNodesCli.mockClear();
|
||||
nodesAction.mockClear();
|
||||
isQaLabCliAvailable.mockReset().mockReturnValue(true);
|
||||
registerQaLabCli.mockClear();
|
||||
registerCapabilityCli.mockClear();
|
||||
inferAction.mockClear();
|
||||
});
|
||||
@@ -98,6 +101,14 @@ describe("registerSubCliCommands", () => {
|
||||
expect(registerAcpCli).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("omits the qa placeholder when the private qa bundle is unavailable", () => {
|
||||
isQaLabCliAvailable.mockReturnValue(false);
|
||||
|
||||
const program = createRegisteredProgram(["node", "openclaw"]);
|
||||
|
||||
expect(program.commands.map((cmd) => cmd.name())).not.toContain("qa");
|
||||
});
|
||||
|
||||
it("re-parses argv for lazy subcommands", async () => {
|
||||
const program = createRegisteredProgram(["node", "openclaw", "nodes", "list"], "openclaw");
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isQaLabCliAvailable } from "../../plugin-sdk/qa-lab.js";
|
||||
import { defineCommandDescriptorCatalog } from "./command-descriptor-utils.js";
|
||||
import type { NamedCommandDescriptor } from "./command-group-descriptors.js";
|
||||
|
||||
@@ -157,9 +158,17 @@ const subCliCommandCatalog = defineCommandDescriptorCatalog([
|
||||
export const SUB_CLI_DESCRIPTORS = subCliCommandCatalog.descriptors;
|
||||
|
||||
export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
|
||||
return subCliCommandCatalog.getDescriptors();
|
||||
const descriptors = subCliCommandCatalog.getDescriptors();
|
||||
if (isQaLabCliAvailable()) {
|
||||
return descriptors;
|
||||
}
|
||||
return descriptors.filter((descriptor) => descriptor.name !== "qa");
|
||||
}
|
||||
|
||||
export function getSubCliCommandsWithSubcommands(): string[] {
|
||||
return subCliCommandCatalog.getCommandsWithSubcommands();
|
||||
const commands = subCliCommandCatalog.getCommandsWithSubcommands();
|
||||
if (isQaLabCliAvailable()) {
|
||||
return commands;
|
||||
}
|
||||
return commands.filter((command) => command !== "qa");
|
||||
}
|
||||
|
||||
@@ -11,3 +11,6 @@ function loadFacadeModule(): FacadeModule {
|
||||
|
||||
export const registerQaLabCli: FacadeModule["registerQaLabCli"] = ((...args) =>
|
||||
loadFacadeModule().registerQaLabCli(...args)) as FacadeModule["registerQaLabCli"];
|
||||
|
||||
export const isQaLabCliAvailable: FacadeModule["isQaLabCliAvailable"] = (() =>
|
||||
loadFacadeModule().isQaLabCliAvailable()) as FacadeModule["isQaLabCliAvailable"];
|
||||
|
||||
@@ -308,6 +308,18 @@ describe("collectForbiddenPackedPathErrors", () => {
|
||||
'npm package must not include generated docs artifact "docs/.generated/config-baseline.plugin.json".',
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects private qa artifacts in npm pack output", () => {
|
||||
expect(
|
||||
collectForbiddenPackedPathErrors([
|
||||
"dist/extensions/qa-channel/package.json",
|
||||
"dist/extensions/qa-lab/src/cli.js",
|
||||
]),
|
||||
).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/src/cli.js".',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectReleaseTagErrors", () => {
|
||||
|
||||
Reference in New Issue
Block a user