mirror of https://github.com/openclaw/openclaw.git
fix: ship bundled plugin runtime sidecars
This commit is contained in:
parent
ffd722bc2c
commit
9334015262
|
|
@ -18,11 +18,14 @@ OpenClaw has three public release lanes:
|
|||
|
||||
- Stable release version: `YYYY.M.D`
|
||||
- Git tag: `vYYYY.M.D`
|
||||
- Stable correction release version: `YYYY.M.D-N`
|
||||
- Git tag: `vYYYY.M.D-N`
|
||||
- Beta prerelease version: `YYYY.M.D-beta.N`
|
||||
- Git tag: `vYYYY.M.D-beta.N`
|
||||
- Do not zero-pad month or day
|
||||
- `latest` means the current stable npm release
|
||||
- `beta` means the current prerelease npm release
|
||||
- Stable correction releases also publish to npm `latest`
|
||||
- Every OpenClaw release ships the npm package and macOS app together
|
||||
|
||||
## Release cadence
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import fs from "node:fs";
|
|||
import path from "node:path";
|
||||
import { shouldBuildBundledCluster } from "./optional-bundled-clusters.mjs";
|
||||
|
||||
const TOP_LEVEL_PUBLIC_SURFACE_EXTENSIONS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]);
|
||||
|
||||
function readBundledPluginPackageJson(packageJsonPath) {
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return null;
|
||||
|
|
@ -30,6 +32,39 @@ function collectPluginSourceEntries(packageJson) {
|
|||
return packageEntries.length > 0 ? packageEntries : ["./index.ts"];
|
||||
}
|
||||
|
||||
function collectTopLevelPublicSurfaceEntries(pluginDir) {
|
||||
if (!fs.existsSync(pluginDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(pluginDir, { withFileTypes: true })
|
||||
.flatMap((dirent) => {
|
||||
if (!dirent.isFile()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ext = path.extname(dirent.name);
|
||||
if (!TOP_LEVEL_PUBLIC_SURFACE_EXTENSIONS.has(ext)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedName = dirent.name.toLowerCase();
|
||||
if (
|
||||
normalizedName.endsWith(".d.ts") ||
|
||||
normalizedName.includes(".test.") ||
|
||||
normalizedName.includes(".spec.") ||
|
||||
normalizedName.includes(".fixture.") ||
|
||||
normalizedName.includes(".snap")
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [`./${dirent.name}`];
|
||||
})
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function collectBundledPluginBuildEntries(params = {}) {
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const env = params.env ?? process.env;
|
||||
|
|
@ -57,7 +92,12 @@ export function collectBundledPluginBuildEntries(params = {}) {
|
|||
id: dirent.name,
|
||||
hasPackageJson: packageJson !== null,
|
||||
packageJson,
|
||||
sourceEntries: collectPluginSourceEntries(packageJson),
|
||||
sourceEntries: Array.from(
|
||||
new Set([
|
||||
...collectPluginSourceEntries(packageJson),
|
||||
...collectTopLevelPublicSurfaceEntries(pluginDir),
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export function collectPublishablePluginPackageErrors(
|
|||
errors.push("package.json version must be non-empty.");
|
||||
} else if (parseReleaseVersion(packageVersion) === null) {
|
||||
errors.push(
|
||||
`package.json version must match YYYY.M.D or YYYY.M.D-beta.N; found "${packageVersion}".`,
|
||||
`package.json version must match YYYY.M.D, YYYY.M.D-N, or YYYY.M.D-beta.N; found "${packageVersion}".`,
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(extensions) || extensions.length === 0) {
|
||||
|
|
@ -224,7 +224,7 @@ export function collectPublishablePluginPackages(
|
|||
const parsedVersion = parseReleaseVersion(version);
|
||||
if (parsedVersion === null) {
|
||||
validationErrors.push(
|
||||
`${dir.name}: package.json version must match YYYY.M.D or YYYY.M.D-beta.N; found "${version}".`,
|
||||
`${dir.name}: package.json version must match YYYY.M.D, YYYY.M.D-N, or YYYY.M.D-beta.N; found "${version}".`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,17 +18,20 @@ type PackageJson = {
|
|||
|
||||
export type ParsedReleaseVersion = {
|
||||
version: string;
|
||||
baseVersion: string;
|
||||
channel: "stable" | "beta";
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
betaNumber?: number;
|
||||
correctionNumber?: number;
|
||||
date: Date;
|
||||
};
|
||||
|
||||
export type ParsedReleaseTag = {
|
||||
version: string;
|
||||
packageVersion: string;
|
||||
baseVersion: string;
|
||||
channel: "stable" | "beta";
|
||||
correctionNumber?: number;
|
||||
date: Date;
|
||||
|
|
@ -37,7 +40,8 @@ export type ParsedReleaseTag = {
|
|||
const STABLE_VERSION_REGEX = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)$/;
|
||||
const BETA_VERSION_REGEX =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<beta>[1-9]\d*)$/;
|
||||
const CORRECTION_TAG_REGEX = /^(?<base>\d{4}\.[1-9]\d?\.[1-9]\d?)-(?<correction>[1-9]\d*)$/;
|
||||
const CORRECTION_VERSION_REGEX =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-(?<correction>[1-9]\d*)$/;
|
||||
const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
|
||||
const MAX_CALVER_DISTANCE_DAYS = 2;
|
||||
const REQUIRED_PACKED_PATHS = ["dist/control-ui/index.html"];
|
||||
|
|
@ -92,6 +96,7 @@ function parseDateParts(
|
|||
|
||||
return {
|
||||
version,
|
||||
baseVersion: `${year}.${month}.${day}`,
|
||||
channel,
|
||||
year,
|
||||
month,
|
||||
|
|
@ -117,6 +122,20 @@ export function parseReleaseVersion(version: string): ParsedReleaseVersion | nul
|
|||
return parseDateParts(trimmed, betaMatch.groups, "beta");
|
||||
}
|
||||
|
||||
const correctionMatch = CORRECTION_VERSION_REGEX.exec(trimmed);
|
||||
if (correctionMatch?.groups) {
|
||||
const parsedCorrection = parseDateParts(trimmed, correctionMatch.groups, "stable");
|
||||
const correctionNumber = Number.parseInt(correctionMatch.groups.correction ?? "", 10);
|
||||
if (parsedCorrection === null || !Number.isInteger(correctionNumber) || correctionNumber < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...parsedCorrection,
|
||||
correctionNumber,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -131,36 +150,14 @@ export function parseReleaseTagVersion(version: string): ParsedReleaseTag | null
|
|||
return {
|
||||
version: trimmed,
|
||||
packageVersion: parsedVersion.version,
|
||||
baseVersion: parsedVersion.baseVersion,
|
||||
channel: parsedVersion.channel,
|
||||
date: parsedVersion.date,
|
||||
correctionNumber: undefined,
|
||||
correctionNumber: parsedVersion.correctionNumber,
|
||||
};
|
||||
}
|
||||
|
||||
const correctionMatch = CORRECTION_TAG_REGEX.exec(trimmed);
|
||||
if (!correctionMatch?.groups) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseVersion = correctionMatch.groups.base ?? "";
|
||||
const parsedBaseVersion = parseReleaseVersion(baseVersion);
|
||||
const correctionNumber = Number.parseInt(correctionMatch.groups.correction ?? "", 10);
|
||||
if (
|
||||
parsedBaseVersion === null ||
|
||||
parsedBaseVersion.channel !== "stable" ||
|
||||
!Number.isInteger(correctionNumber) ||
|
||||
correctionNumber < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
version: trimmed,
|
||||
packageVersion: parsedBaseVersion.version,
|
||||
channel: "stable",
|
||||
correctionNumber,
|
||||
date: parsedBaseVersion.date,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
function startOfUtcDay(date: Date): number {
|
||||
|
|
@ -227,7 +224,7 @@ export function collectReleaseTagErrors(params: {
|
|||
const parsedVersion = parseReleaseVersion(packageVersion);
|
||||
if (parsedVersion === null) {
|
||||
errors.push(
|
||||
`package.json version must match YYYY.M.D or YYYY.M.D-beta.N; found "${packageVersion || "<missing>"}".`,
|
||||
`package.json version must match YYYY.M.D, YYYY.M.D-N, or YYYY.M.D-beta.N; found "${packageVersion || "<missing>"}".`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -244,17 +241,24 @@ export function collectReleaseTagErrors(params: {
|
|||
}
|
||||
|
||||
const expectedTag = packageVersion ? `v${packageVersion}` : "<missing>";
|
||||
const expectedCorrectionTag = parsedVersion?.channel === "stable" ? `${expectedTag}-N` : null;
|
||||
const matchesExpectedTag =
|
||||
parsedTag !== null &&
|
||||
parsedVersion !== null &&
|
||||
parsedTag.packageVersion === parsedVersion.version &&
|
||||
parsedTag.channel === parsedVersion.channel;
|
||||
parsedTag.channel === parsedVersion.channel &&
|
||||
(parsedTag.packageVersion === parsedVersion.version ||
|
||||
(parsedVersion.channel === "stable" &&
|
||||
parsedVersion.correctionNumber === undefined &&
|
||||
parsedTag.correctionNumber !== undefined &&
|
||||
parsedTag.baseVersion === parsedVersion.baseVersion));
|
||||
if (!matchesExpectedTag) {
|
||||
errors.push(
|
||||
`Release tag ${releaseTag || "<missing>"} does not match package.json version ${
|
||||
packageVersion || "<missing>"
|
||||
}; expected ${expectedCorrectionTag ? `${expectedTag} or ${expectedCorrectionTag}` : expectedTag}.`,
|
||||
}; expected ${
|
||||
parsedVersion?.channel === "stable" && parsedVersion.correctionNumber === undefined
|
||||
? `${expectedTag} or ${expectedTag}-N`
|
||||
: expectedTag
|
||||
}.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,35 @@ describe("stageBundledPluginRuntime", () => {
|
|||
expect(runtimeModule.value).toBe(1);
|
||||
});
|
||||
|
||||
it("stages root runtime sidecars that bundled plugin boundaries resolve directly", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-sidecars-");
|
||||
const distPluginDir = path.join(repoRoot, "dist", "extensions", "whatsapp");
|
||||
fs.mkdirSync(distPluginDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(distPluginDir, "index.js"), "export default {};\n", "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(distPluginDir, "light-runtime-api.js"),
|
||||
"export const light = true;\n",
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(distPluginDir, "runtime-api.js"),
|
||||
"export const heavy = true;\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
stageBundledPluginRuntime({ repoRoot });
|
||||
|
||||
const runtimePluginDir = path.join(repoRoot, "dist-runtime", "extensions", "whatsapp");
|
||||
expect(fs.existsSync(path.join(runtimePluginDir, "light-runtime-api.js"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(runtimePluginDir, "runtime-api.js"))).toBe(true);
|
||||
expect(fs.readFileSync(path.join(runtimePluginDir, "light-runtime-api.js"), "utf8")).toContain(
|
||||
"../../../dist/extensions/whatsapp/light-runtime-api.js",
|
||||
);
|
||||
expect(fs.readFileSync(path.join(runtimePluginDir, "runtime-api.js"), "utf8")).toContain(
|
||||
"../../../dist/extensions/whatsapp/runtime-api.js",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps plugin command registration on the canonical dist graph when loaded from dist-runtime", async () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-commands-");
|
||||
const distPluginDir = path.join(repoRoot, "dist", "extensions", "demo");
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ describe("parseReleaseVersion", () => {
|
|||
it("parses stable CalVer releases", () => {
|
||||
expect(parseReleaseVersion("2026.3.10")).toMatchObject({
|
||||
version: "2026.3.10",
|
||||
baseVersion: "2026.3.10",
|
||||
channel: "stable",
|
||||
year: 2026,
|
||||
month: 3,
|
||||
|
|
@ -24,6 +25,7 @@ describe("parseReleaseVersion", () => {
|
|||
it("parses beta CalVer releases", () => {
|
||||
expect(parseReleaseVersion("2026.3.10-beta.2")).toMatchObject({
|
||||
version: "2026.3.10-beta.2",
|
||||
baseVersion: "2026.3.10",
|
||||
channel: "beta",
|
||||
year: 2026,
|
||||
month: 3,
|
||||
|
|
@ -32,20 +34,33 @@ describe("parseReleaseVersion", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("parses stable correction releases", () => {
|
||||
expect(parseReleaseVersion("2026.3.10-1")).toMatchObject({
|
||||
version: "2026.3.10-1",
|
||||
baseVersion: "2026.3.10",
|
||||
channel: "stable",
|
||||
year: 2026,
|
||||
month: 3,
|
||||
day: 10,
|
||||
correctionNumber: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects legacy and malformed release formats", () => {
|
||||
expect(parseReleaseVersion("2026.3.10-1")).toBeNull();
|
||||
expect(parseReleaseVersion("2026.03.09")).toBeNull();
|
||||
expect(parseReleaseVersion("v2026.3.10")).toBeNull();
|
||||
expect(parseReleaseVersion("2026.2.30")).toBeNull();
|
||||
expect(parseReleaseVersion("2026.3.10-0")).toBeNull();
|
||||
expect(parseReleaseVersion("2.0.0-beta2")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseReleaseTagVersion", () => {
|
||||
it("accepts fallback correction tags for stable releases", () => {
|
||||
it("accepts correction release tags", () => {
|
||||
expect(parseReleaseTagVersion("2026.3.10-2")).toMatchObject({
|
||||
version: "2026.3.10-2",
|
||||
packageVersion: "2026.3.10",
|
||||
packageVersion: "2026.3.10-2",
|
||||
baseVersion: "2026.3.10",
|
||||
channel: "stable",
|
||||
correctionNumber: 2,
|
||||
});
|
||||
|
|
@ -180,6 +195,16 @@ describe("collectReleaseTagErrors", () => {
|
|||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("accepts correction package versions paired with matching correction tags", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
packageVersion: "2026.3.10-1",
|
||||
releaseTag: "v2026.3.10-1",
|
||||
now: new Date("2026-03-10T00:00:00Z"),
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects beta package versions paired with fallback correction tags", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ describe("collectPublishablePluginPackageErrors", () => {
|
|||
).toEqual([
|
||||
'package name must start with "@openclaw/"; found "broken".',
|
||||
"package.json private must not be true.",
|
||||
'package.json version must match YYYY.M.D or YYYY.M.D-beta.N; found "latest".',
|
||||
'package.json version must match YYYY.M.D, YYYY.M.D-N, or YYYY.M.D-beta.N; found "latest".',
|
||||
"openclaw.extensions must contain only non-empty strings.",
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -137,8 +137,13 @@ describe("collectMissingPackPaths", () => {
|
|||
expect.arrayContaining([
|
||||
"dist/channel-catalog.json",
|
||||
"dist/control-ui/index.html",
|
||||
"dist/extensions/matrix/helper-api.js",
|
||||
"dist/extensions/matrix/runtime-api.js",
|
||||
"dist/extensions/matrix/thread-bindings-runtime.js",
|
||||
"dist/extensions/matrix/openclaw.plugin.json",
|
||||
"dist/extensions/matrix/package.json",
|
||||
"dist/extensions/whatsapp/light-runtime-api.js",
|
||||
"dist/extensions/whatsapp/runtime-api.js",
|
||||
"dist/extensions/whatsapp/openclaw.plugin.json",
|
||||
"dist/extensions/whatsapp/package.json",
|
||||
]),
|
||||
|
|
@ -159,6 +164,18 @@ describe("collectMissingPackPaths", () => {
|
|||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("requires bundled plugin runtime sidecars that dynamic plugin boundaries resolve at runtime", () => {
|
||||
expect(requiredBundledPluginPackPaths).toEqual(
|
||||
expect.arrayContaining([
|
||||
"dist/extensions/matrix/helper-api.js",
|
||||
"dist/extensions/matrix/runtime-api.js",
|
||||
"dist/extensions/matrix/thread-bindings-runtime.js",
|
||||
"dist/extensions/whatsapp/light-runtime-api.js",
|
||||
"dist/extensions/whatsapp/runtime-api.js",
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPackUnpackedSizeErrors", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue