Skip to content

Commit f493b03

Browse files
committed
refactor: validate bundled extension release metadata
1 parent e53d840 commit f493b03

File tree

2 files changed

+109
-8
lines changed

2 files changed

+109
-8
lines changed

scripts/release-check.ts

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ type PackageJson = {
2222
};
2323
};
2424
};
25+
type BundledExtension = { id: string; packageJson: PackageJson };
26+
type BundledExtensionMetadata = BundledExtension & {
27+
npmSpec?: string;
28+
rootDependencyMirrorAllowlist: string[];
29+
};
2530

2631
const requiredPathGroups = [
2732
["dist/index.js", "dist/index.mjs"],
@@ -133,25 +138,23 @@ function normalizePluginSyncVersion(version: string): string {
133138

134139
export function collectBundledExtensionRootDependencyGapErrors(params: {
135140
rootPackage: PackageJson;
136-
extensions: Array<{ id: string; packageJson: PackageJson }>;
141+
extensions: BundledExtension[];
137142
}): string[] {
138143
const rootDeps = {
139144
...params.rootPackage.dependencies,
140145
...params.rootPackage.optionalDependencies,
141146
};
142147
const errors: string[] = [];
143148

144-
for (const extension of params.extensions) {
145-
if (!extension.packageJson.openclaw?.install?.npmSpec) {
149+
for (const extension of normalizeBundledExtensionMetadata(params.extensions)) {
150+
if (!extension.npmSpec) {
146151
continue;
147152
}
148153

149154
const missing = Object.keys(extension.packageJson.dependencies ?? {})
150155
.filter((dep) => dep !== "openclaw" && !rootDeps[dep])
151156
.toSorted();
152-
const allowlisted = [
153-
...(extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist ?? []),
154-
].toSorted();
157+
const allowlisted = extension.rootDependencyMirrorAllowlist.toSorted();
155158
if (missing.join("\n") !== allowlisted.join("\n")) {
156159
const unexpected = missing.filter((dep) => !allowlisted.includes(dep));
157160
const resolved = allowlisted.filter((dep) => !missing.includes(dep));
@@ -172,7 +175,56 @@ export function collectBundledExtensionRootDependencyGapErrors(params: {
172175
return errors;
173176
}
174177

175-
function collectBundledExtensions(): Array<{ id: string; packageJson: PackageJson }> {
178+
function normalizeBundledExtensionMetadata(
179+
extensions: BundledExtension[],
180+
): BundledExtensionMetadata[] {
181+
return extensions.map((extension) => ({
182+
...extension,
183+
npmSpec:
184+
typeof extension.packageJson.openclaw?.install?.npmSpec === "string"
185+
? extension.packageJson.openclaw.install.npmSpec.trim()
186+
: undefined,
187+
rootDependencyMirrorAllowlist:
188+
extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist?.filter(
189+
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
190+
) ?? [],
191+
}));
192+
}
193+
194+
export function collectBundledExtensionManifestErrors(extensions: BundledExtension[]): string[] {
195+
const errors: string[] = [];
196+
for (const extension of extensions) {
197+
const install = extension.packageJson.openclaw?.install;
198+
if (
199+
install &&
200+
(!install.npmSpec || typeof install.npmSpec !== "string" || !install.npmSpec.trim())
201+
) {
202+
errors.push(
203+
`bundled extension '${extension.id}' manifest invalid | openclaw.install.npmSpec must be a non-empty string`,
204+
);
205+
}
206+
207+
const allowlist = extension.packageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
208+
if (allowlist === undefined) {
209+
continue;
210+
}
211+
if (!Array.isArray(allowlist)) {
212+
errors.push(
213+
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array of non-empty strings`,
214+
);
215+
continue;
216+
}
217+
const invalidEntries = allowlist.filter((entry) => typeof entry !== "string" || !entry.trim());
218+
if (invalidEntries.length > 0) {
219+
errors.push(
220+
`bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must contain only non-empty strings`,
221+
);
222+
}
223+
}
224+
return errors;
225+
}
226+
227+
function collectBundledExtensions(): BundledExtension[] {
176228
const extensionsDir = resolve("extensions");
177229
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
178230
entry.isDirectory(),
@@ -195,9 +247,18 @@ function collectBundledExtensions(): Array<{ id: string; packageJson: PackageJso
195247

196248
function checkBundledExtensionRootDependencyMirrors() {
197249
const rootPackage = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as PackageJson;
250+
const extensions = collectBundledExtensions();
251+
const manifestErrors = collectBundledExtensionManifestErrors(extensions);
252+
if (manifestErrors.length > 0) {
253+
console.error("release-check: bundled extension manifest validation failed:");
254+
for (const error of manifestErrors) {
255+
console.error(` - ${error}`);
256+
}
257+
process.exit(1);
258+
}
198259
const errors = collectBundledExtensionRootDependencyGapErrors({
199260
rootPackage,
200-
extensions: collectBundledExtensions(),
261+
extensions,
201262
});
202263
if (errors.length > 0) {
203264
console.error("release-check: bundled extension root dependency mirror validation failed:");

test/release-check.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from "vitest";
22
import {
33
collectAppcastSparkleVersionErrors,
4+
collectBundledExtensionManifestErrors,
45
collectBundledExtensionRootDependencyGapErrors,
56
} from "../scripts/release-check.ts";
67

@@ -110,3 +111,42 @@ describe("collectBundledExtensionRootDependencyGapErrors", () => {
110111
]);
111112
});
112113
});
114+
115+
describe("collectBundledExtensionManifestErrors", () => {
116+
it("flags invalid bundled extension install metadata", () => {
117+
expect(
118+
collectBundledExtensionManifestErrors([
119+
{
120+
id: "broken",
121+
packageJson: {
122+
openclaw: {
123+
install: { npmSpec: " " },
124+
},
125+
},
126+
},
127+
]),
128+
).toEqual([
129+
"bundled extension 'broken' manifest invalid | openclaw.install.npmSpec must be a non-empty string",
130+
]);
131+
});
132+
133+
it("flags invalid release-check allowlist metadata", () => {
134+
expect(
135+
collectBundledExtensionManifestErrors([
136+
{
137+
id: "broken",
138+
packageJson: {
139+
openclaw: {
140+
install: { npmSpec: "@openclaw/broken" },
141+
releaseChecks: {
142+
rootDependencyMirrorAllowlist: ["ok", ""],
143+
},
144+
},
145+
},
146+
},
147+
]),
148+
).toEqual([
149+
"bundled extension 'broken' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must contain only non-empty strings",
150+
]);
151+
});
152+
});

0 commit comments

Comments
 (0)