fix(release): tolerate legacy installed plugin min host floors

This commit is contained in:
Peter Steinberger
2026-05-02 03:18:26 +01:00
parent 01c5df6a4e
commit ed214817fb
4 changed files with 77 additions and 3 deletions

View File

@@ -1528,6 +1528,41 @@ describe("loadPluginManifestRegistry", () => {
}
});
it("accepts legacy bare minHostVersion metadata for recorded installed globals", () => {
const dir = makeTempDir();
writeManifest(dir, { id: "codex", configSchema: { type: "object" } });
const registry = loadPluginManifestRegistry({
installRecords: {
codex: {
source: "npm",
installPath: dir,
},
},
candidates: [
createPluginCandidate({
idHint: "codex",
rootDir: dir,
packageDir: dir,
origin: "global",
packageManifest: {
install: {
npmSpec: "@openclaw/codex",
minHostVersion: "2026.3.22",
},
},
}),
],
});
expect(registry.plugins.map((plugin) => plugin.id)).toEqual(["codex"]);
expect(
registry.diagnostics.some((diag) =>
diag.message.includes("openclaw.install.minHostVersion must use"),
),
).toBe(false);
});
it.each([
{
name: "reports bundled plugins as the duplicate winner for auto-discovered globals",

View File

@@ -617,9 +617,19 @@ export function loadPluginManifestRegistry(
continue;
}
const manifest = manifestRes.manifest;
const allowLegacyBareMinHostVersion =
candidate.origin === "global" &&
matchesInstalledPluginRecord({
pluginId: manifest.id,
candidate,
config,
env,
installRecords: getInstallRecords(),
});
const minHostVersionCheck = checkMinHostVersion({
currentVersion: currentHostVersion,
minHostVersion: candidate.packageManifest?.install?.minHostVersion,
allowLegacyBareSemver: allowLegacyBareMinHostVersion,
});
if (!minHostVersionCheck.ok) {
const packageManifestSource = path.join(

View File

@@ -59,6 +59,26 @@ describe("min-host-version", () => {
expect(parseMinHostVersionRequirement(">=2026.3.22")).toEqual(MIN_HOST_REQUIREMENT);
});
it("can parse legacy bare semver floors for runtime upgrade compatibility", () => {
expect(parseMinHostVersionRequirement("2026.3.22", { allowLegacyBareSemver: true })).toEqual({
raw: "2026.3.22",
minimumLabel: "2026.3.22",
});
expect(
checkMinHostVersion({
currentVersion: "2026.3.22",
minHostVersion: "2026.3.22",
allowLegacyBareSemver: true,
}),
).toEqual({
ok: true,
requirement: {
raw: "2026.3.22",
minimumLabel: "2026.3.22",
},
});
});
it.each(["2026.3.22", 123, ">=2026.3.22 garbage"] as const)(
"rejects invalid floor syntax and host checks: %p",
(minHostVersion) => {

View File

@@ -3,6 +3,7 @@ import { isAtLeast, parseSemver } from "../infra/runtime-guard.js";
export const MIN_HOST_VERSION_FORMAT =
'openclaw.install.minHostVersion must use a semver floor in the form ">=x.y.z"';
const MIN_HOST_VERSION_RE = /^>=(\d+)\.(\d+)\.(\d+)$/;
const LEGACY_MIN_HOST_VERSION_RE = /^(\d+)\.(\d+)\.(\d+)$/;
export type MinHostVersionRequirement = {
raw: string;
@@ -22,7 +23,10 @@ export type MinHostVersionCheckResult =
currentVersion: string;
};
export function parseMinHostVersionRequirement(raw: unknown): MinHostVersionRequirement | null {
export function parseMinHostVersionRequirement(
raw: unknown,
options: { allowLegacyBareSemver?: boolean } = {},
): MinHostVersionRequirement | null {
if (typeof raw !== "string") {
return null;
}
@@ -30,7 +34,9 @@ export function parseMinHostVersionRequirement(raw: unknown): MinHostVersionRequ
if (!trimmed) {
return null;
}
const match = trimmed.match(MIN_HOST_VERSION_RE);
const match =
trimmed.match(MIN_HOST_VERSION_RE) ??
(options.allowLegacyBareSemver ? trimmed.match(LEGACY_MIN_HOST_VERSION_RE) : null);
if (!match) {
return null;
}
@@ -54,11 +60,14 @@ export function validateMinHostVersion(raw: unknown): string | null {
export function checkMinHostVersion(params: {
currentVersion: string | undefined;
minHostVersion: unknown;
allowLegacyBareSemver?: boolean;
}): MinHostVersionCheckResult {
if (params.minHostVersion === undefined) {
return { ok: true, requirement: null };
}
const requirement = parseMinHostVersionRequirement(params.minHostVersion);
const requirement = parseMinHostVersionRequirement(params.minHostVersion, {
allowLegacyBareSemver: params.allowLegacyBareSemver,
});
if (!requirement) {
return { ok: false, kind: "invalid", error: MIN_HOST_VERSION_FORMAT };
}