mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
ci(deps): gate extension-owned root dependencies
This commit is contained in:
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -1642,6 +1642,7 @@ jobs:
|
|||||||
run_check "lint:plugins:no-extension-src-imports" pnpm run lint:plugins:no-extension-src-imports
|
run_check "lint:plugins:no-extension-src-imports" pnpm run lint:plugins:no-extension-src-imports
|
||||||
run_check "lint:plugins:no-extension-test-core-imports" pnpm run lint:plugins:no-extension-test-core-imports
|
run_check "lint:plugins:no-extension-test-core-imports" pnpm run lint:plugins:no-extension-test-core-imports
|
||||||
run_check "lint:plugins:plugin-sdk-subpaths-exported" pnpm run lint:plugins:plugin-sdk-subpaths-exported
|
run_check "lint:plugins:plugin-sdk-subpaths-exported" pnpm run lint:plugins:plugin-sdk-subpaths-exported
|
||||||
|
run_check "deps:root-ownership:check" pnpm deps:root-ownership:check
|
||||||
run_check "web-search-provider-boundary" pnpm run lint:web-search-provider-boundaries
|
run_check "web-search-provider-boundary" pnpm run lint:web-search-provider-boundaries
|
||||||
run_check "web-fetch-provider-boundary" pnpm run lint:web-fetch-provider-boundaries
|
run_check "web-fetch-provider-boundary" pnpm run lint:web-fetch-provider-boundaries
|
||||||
run_check "extension-src-outside-plugin-sdk-boundary" pnpm run lint:extensions:no-src-outside-plugin-sdk
|
run_check "extension-src-outside-plugin-sdk-boundary" pnpm run lint:extensions:no-src-outside-plugin-sdk
|
||||||
|
|||||||
@@ -1282,6 +1282,7 @@
|
|||||||
"deadcode:ts-prune": "pnpm dlx ts-prune src extensions scripts",
|
"deadcode:ts-prune": "pnpm dlx ts-prune src extensions scripts",
|
||||||
"deadcode:ts-unused": "pnpm dlx ts-unused-exports tsconfig.json --ignoreTestFiles --exitWithCount",
|
"deadcode:ts-unused": "pnpm dlx ts-unused-exports tsconfig.json --ignoreTestFiles --exitWithCount",
|
||||||
"deps:root-ownership": "node scripts/root-dependency-ownership-audit.mjs",
|
"deps:root-ownership": "node scripts/root-dependency-ownership-audit.mjs",
|
||||||
|
"deps:root-ownership:check": "node scripts/root-dependency-ownership-audit.mjs --check",
|
||||||
"dev": "node scripts/run-node.mjs",
|
"dev": "node scripts/run-node.mjs",
|
||||||
"docs:bin": "node scripts/build-docs-list.mjs",
|
"docs:bin": "node scripts/build-docs-list.mjs",
|
||||||
"docs:check-i18n-glossary": "node scripts/check-docs-i18n-glossary.mjs",
|
"docs:check-i18n-glossary": "node scripts/check-docs-i18n-glossary.mjs",
|
||||||
@@ -1382,7 +1383,7 @@
|
|||||||
"qa:lab:up": "node --import tsx scripts/qa-lab-up.ts",
|
"qa:lab:up": "node --import tsx scripts/qa-lab-up.ts",
|
||||||
"qa:lab:up:fast": "node --import tsx scripts/qa-lab-up.ts --use-prebuilt-image --bind-ui-dist --skip-ui-build",
|
"qa:lab:up:fast": "node --import tsx scripts/qa-lab-up.ts --use-prebuilt-image --bind-ui-dist --skip-ui-build",
|
||||||
"qa:lab:watch": "vite build --watch --config extensions/qa-lab/web/vite.config.ts",
|
"qa:lab:watch": "vite build --watch --config extensions/qa-lab/web/vite.config.ts",
|
||||||
"release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:api:check && node --import tsx scripts/release-check.ts",
|
"release:check": "pnpm deps:root-ownership:check && pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:api:check && node --import tsx scripts/release-check.ts",
|
||||||
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
|
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
|
||||||
"release:openclaw:npm:verify-published": "node --import tsx scripts/openclaw-npm-postpublish-verify.ts",
|
"release:openclaw:npm:verify-published": "node --import tsx scripts/openclaw-npm-postpublish-verify.ts",
|
||||||
"release:plugins:clawhub:check": "node --import tsx scripts/plugin-clawhub-release-check.ts",
|
"release:plugins:clawhub:check": "node --import tsx scripts/plugin-clawhub-release-check.ts",
|
||||||
|
|||||||
@@ -268,6 +268,23 @@ export function collectRootDependencyOwnershipAudit(params = {}) {
|
|||||||
.toSorted((left, right) => left.depName.localeCompare(right.depName));
|
.toSorted((left, right) => left.depName.localeCompare(right.depName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function collectRootDependencyOwnershipCheckErrors(records) {
|
||||||
|
return records
|
||||||
|
.filter((record) => record.category === "extension_only_localizable")
|
||||||
|
.map((record) => {
|
||||||
|
const declaredInExtensions =
|
||||||
|
record.declaredInExtensions.length > 0
|
||||||
|
? `; extension declarations: ${record.declaredInExtensions.join(", ")}`
|
||||||
|
: "";
|
||||||
|
const sampleFiles =
|
||||||
|
record.sampleFiles.length > 0 ? `; sample imports: ${record.sampleFiles.join(", ")}` : "";
|
||||||
|
return (
|
||||||
|
`root dependency '${record.depName}' is extension-owned (${record.recommendation})` +
|
||||||
|
`${declaredInExtensions}${sampleFiles}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function printTextReport(records) {
|
function printTextReport(records) {
|
||||||
const grouped = new Map();
|
const grouped = new Map();
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
@@ -294,7 +311,22 @@ function printTextReport(records) {
|
|||||||
|
|
||||||
function main(argv = process.argv.slice(2)) {
|
function main(argv = process.argv.slice(2)) {
|
||||||
const asJson = argv.includes("--json");
|
const asJson = argv.includes("--json");
|
||||||
|
const check = argv.includes("--check");
|
||||||
const records = collectRootDependencyOwnershipAudit();
|
const records = collectRootDependencyOwnershipAudit();
|
||||||
|
if (check) {
|
||||||
|
const errors = collectRootDependencyOwnershipCheckErrors(records);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
for (const error of errors) {
|
||||||
|
console.error(`[root-dependency-ownership] ${error}`);
|
||||||
|
}
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!asJson) {
|
||||||
|
console.error("[root-dependency-ownership] ok");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (asJson) {
|
if (asJson) {
|
||||||
console.log(JSON.stringify(records, null, 2));
|
console.log(JSON.stringify(records, null, 2));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,9 +1,34 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
classifyRootDependencyOwnership,
|
classifyRootDependencyOwnership,
|
||||||
|
collectRootDependencyOwnershipAudit,
|
||||||
|
collectRootDependencyOwnershipCheckErrors,
|
||||||
collectModuleSpecifiers,
|
collectModuleSpecifiers,
|
||||||
} from "../../scripts/root-dependency-ownership-audit.mjs";
|
} from "../../scripts/root-dependency-ownership-audit.mjs";
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
rmSync(dir, { force: true, recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeTempRepo() {
|
||||||
|
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-root-deps-audit-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRepoFile(repoRoot: string, relativePath: string, value: string) {
|
||||||
|
const filePath = path.join(repoRoot, relativePath);
|
||||||
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
writeFileSync(filePath, value, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
describe("collectModuleSpecifiers", () => {
|
describe("collectModuleSpecifiers", () => {
|
||||||
it("captures require.resolve package lookups used by runtime shims and bundled plugins", () => {
|
it("captures require.resolve package lookups used by runtime shims and bundled plugins", () => {
|
||||||
expect([
|
expect([
|
||||||
@@ -80,3 +105,54 @@ describe("classifyRootDependencyOwnership", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("collectRootDependencyOwnershipCheckErrors", () => {
|
||||||
|
it("catches dependencies mirrored at root but only imported by one extension", () => {
|
||||||
|
const repoRoot = makeTempRepo();
|
||||||
|
writeRepoFile(
|
||||||
|
repoRoot,
|
||||||
|
"package.json",
|
||||||
|
JSON.stringify({ dependencies: { "vendor-sdk": "^1.0.0" } }),
|
||||||
|
);
|
||||||
|
writeRepoFile(
|
||||||
|
repoRoot,
|
||||||
|
"extensions/qqbot/package.json",
|
||||||
|
JSON.stringify({ dependencies: { "vendor-sdk": "^1.0.0" } }),
|
||||||
|
);
|
||||||
|
writeRepoFile(
|
||||||
|
repoRoot,
|
||||||
|
"extensions/qqbot/src/setup.ts",
|
||||||
|
'const sdk = await import("vendor-sdk");\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
const records = collectRootDependencyOwnershipAudit({ repoRoot, scanRoots: ["extensions"] });
|
||||||
|
|
||||||
|
expect(collectRootDependencyOwnershipCheckErrors(records)).toEqual([
|
||||||
|
"root dependency 'vendor-sdk' is extension-owned (remove from root package.json and rely on owning extension manifests plus doctor --fix); extension declarations: qqbot:dependencies; sample imports: extensions/qqbot/src/setup.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails only extension-owned root dependencies", () => {
|
||||||
|
expect(
|
||||||
|
collectRootDependencyOwnershipCheckErrors([
|
||||||
|
{
|
||||||
|
category: "extension_only_localizable",
|
||||||
|
declaredInExtensions: ["qqbot:dependencies"],
|
||||||
|
depName: "@tencent-connect/qqbot-connector",
|
||||||
|
recommendation:
|
||||||
|
"remove from root package.json and rely on owning extension manifests plus doctor --fix",
|
||||||
|
sampleFiles: ["extensions/qqbot/src/bridge/setup/finalize.ts"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "unreferenced",
|
||||||
|
declaredInExtensions: [],
|
||||||
|
depName: "@mozilla/readability",
|
||||||
|
recommendation: "investigate removal; no direct source imports found in scanned files",
|
||||||
|
sampleFiles: [],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toEqual([
|
||||||
|
"root dependency '@tencent-connect/qqbot-connector' is extension-owned (remove from root package.json and rely on owning extension manifests plus doctor --fix); extension declarations: qqbot:dependencies; sample imports: extensions/qqbot/src/bridge/setup/finalize.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user