openclaw/scripts/sync-plugin-versions.ts
Peter Steinberger cfef4085b8
fix: preserve higher plugin API requirements during release sync (#154640)
* fix(release): preserve higher plugin API floors

Raise lower plugin API requirements during release alignment without lowering intentional floors for newer host APIs. Reuse the shared OpenClaw semver ordering for beta, RC, and correction releases, and preserve invalid declarations.

Add filesystem-boundary regression cases for check and write modes, and document floor preservation. The generator suite passes all 16 tests; focused cost is 3.01 seconds wall. P2 review and changed-file checks pass. Package versions, dependencies, host floors, manifests, and lockfiles are unchanged.

* chore: integrate main for plugin API floor validation

* test: retain PR controller output on close failure

Print the existing child output buffer if the fresh-main fixture cannot observe controller close, then rethrow the original error. Preserve the existing deadlines and joined cleanup. The earlier hosted timeouts remain unexplained.

* chore: integrate main after Gateway shard rebalance

Include the canonical worker-environments shard rebalance from #155734. Preserve the exact reviewed five-file API-floor and controller-output patch. The catalog timeout and earlier controller-close timeouts remain separately qualified.

* chore: integrate main after archive fixture repair

Include the canonical archive lifecycle fixture repair while preserving the exact reviewed five-file API-floor and controller-output patch. Retain prior failed CI and the noncausal catalog pass as qualified evidence.

* chore: preserve fixture diagnostics across main integration

Keep the upstream private-handoff setup and injection assertion alongside the unchanged controller-output catch. Preserve main and the original API-floor repair without adding timeout or retry policy.

* chore: integrate the canonical launcher fixture repair

* chore: integrate the canonical wrapper closure repair

* chore: integrate canonical wrapper and worker bundle fixes

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-09-22 12:36:45 -07:00

215 lines
6.5 KiB
TypeScript

// Sync Plugin Versions script supports OpenClaw repository automation.
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { parse as parseSemver } from "semver";
import { compareOpenClawSemver } from "../src/infra/semver.js";
type PackageJson = {
name?: string;
version?: string;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
openclaw?: {
install?: {
minHostVersion?: string;
};
compat?: {
pluginApi?: string;
};
build?: {
openclawVersion?: string;
};
};
};
type SyncPluginVersionsOptions = {
write?: boolean;
};
const OPENCLAW_VERSION_RANGE_RE = /^>=\d{4}\.\d{1,2}\.\d{1,2}(?:[-.][^"\s]+)?$/u;
const VERSION_ALIGNED_PACKAGE_DIRS = [
"packages/ai",
"packages/gateway-client",
"packages/gateway-protocol",
] as const;
function syncOpenClawDependencyRange(
deps: Record<string, string> | undefined,
targetVersion: string,
): boolean {
const current = deps?.openclaw;
if (!current || current === "workspace:*" || !OPENCLAW_VERSION_RANGE_RE.test(current)) {
return false;
}
const next = `>=${targetVersion}`;
if (current === next) {
return false;
}
deps.openclaw = next;
return true;
}
function syncPluginApiVersion(pkg: PackageJson, targetVersion: string): boolean {
const compat = pkg.openclaw?.compat;
const current = compat?.pluginApi;
if (!current || !OPENCLAW_VERSION_RANGE_RE.test(current)) {
return false;
}
const currentVersion = parseSemver(current.slice(2));
const nextVersion = parseSemver(targetVersion);
if (!currentVersion || !nextVersion || compareOpenClawSemver(nextVersion, currentVersion) <= 0) {
return false;
}
compat.pluginApi = `>=${targetVersion}`;
return true;
}
function syncBuildOpenClawVersion(pkg: PackageJson, targetVersion: string): boolean {
const build = pkg.openclaw?.build;
const current = build?.openclawVersion;
if (!current) {
return false;
}
if (current === targetVersion) {
return false;
}
build.openclawVersion = targetVersion;
return true;
}
function changelogVersionForPackageVersion(version: string): string {
return version.replace(/-beta\.\d+$/u, "");
}
function ensureChangelogEntry(changelogPath: string, version: string, write: boolean): boolean {
if (!existsSync(changelogPath)) {
return false;
}
const content = readFileSync(changelogPath, "utf8");
if (content.includes(`## ${version}`)) {
return false;
}
const entry = `## ${version}\n\n### Changes\n- Version alignment with core OpenClaw release numbers.\n\n`;
if (content.startsWith("# Changelog\n\n")) {
const next = content.replace("# Changelog\n\n", `# Changelog\n\n${entry}`);
if (write) {
writeFileSync(changelogPath, next);
}
return true;
}
const next = `# Changelog\n\n${entry}${content.trimStart()}`;
if (write) {
writeFileSync(changelogPath, `${next}\n`);
}
return true;
}
export function syncPluginVersions(
rootDir = resolve("."),
options: SyncPluginVersionsOptions = {},
) {
const write = options.write ?? true;
const rootPackagePath = join(rootDir, "package.json");
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8")) as PackageJson;
const targetVersion = rootPackage.version;
if (!targetVersion) {
throw new Error("Root package.json missing version.");
}
const extensionsDir = join(rootDir, "extensions");
const dirs = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
entry.isDirectory(),
);
const updated: string[] = [];
const changelogged: string[] = [];
const skipped: string[] = [];
for (const packageDir of VERSION_ALIGNED_PACKAGE_DIRS) {
const packagePath = join(rootDir, packageDir, "package.json");
if (!existsSync(packagePath)) {
continue;
}
const pkg = JSON.parse(readFileSync(packagePath, "utf8")) as PackageJson;
if (!pkg.name || pkg.version === targetVersion) {
continue;
}
pkg.version = targetVersion;
if (write) {
writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
}
updated.push(pkg.name);
}
for (const dir of dirs) {
const packagePath = join(extensionsDir, dir.name, "package.json");
let pkg: PackageJson;
try {
pkg = JSON.parse(readFileSync(packagePath, "utf8")) as PackageJson;
} catch {
continue;
}
if (!pkg.name) {
skipped.push(dir.name);
continue;
}
const changelogPath = join(extensionsDir, dir.name, "CHANGELOG.md");
const changelogVersion = changelogVersionForPackageVersion(targetVersion);
if (ensureChangelogEntry(changelogPath, changelogVersion, write)) {
changelogged.push(pkg.name);
}
const versionChanged = pkg.version !== targetVersion;
const devDependencyChanged = syncOpenClawDependencyRange(pkg.devDependencies, targetVersion);
const peerDependencyChanged = syncOpenClawDependencyRange(pkg.peerDependencies, targetVersion);
// minHostVersion is a compatibility floor, not release alignment metadata.
// Keep it stable unless the owning plugin intentionally raises it.
const pluginApiChanged = syncPluginApiVersion(pkg, targetVersion);
const buildOpenClawVersionChanged = syncBuildOpenClawVersion(pkg, targetVersion);
const packageChanged =
versionChanged ||
devDependencyChanged ||
peerDependencyChanged ||
pluginApiChanged ||
buildOpenClawVersionChanged;
if (!packageChanged) {
skipped.push(pkg.name);
continue;
}
if (versionChanged) {
pkg.version = targetVersion;
}
if (write) {
writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
}
updated.push(pkg.name);
}
return {
targetVersion,
updated,
changelogged,
skipped,
};
}
if (import.meta.main) {
const check = process.argv.includes("--check");
const summary = syncPluginVersions(resolve("."), { write: !check });
console.log(
`Synced plugin versions to ${summary.targetVersion}. Updated: ${summary.updated.length}. Changelogged: ${summary.changelogged.length}. Skipped: ${summary.skipped.length}.`,
);
if (check && (summary.updated.length > 0 || summary.changelogged.length > 0)) {
for (const packageName of summary.updated) {
console.error(` update required: ${packageName}`);
}
for (const packageName of summary.changelogged) {
console.error(` changelog entry required: ${packageName}`);
}
console.error("Run `pnpm plugins:sync` and commit the plugin version alignment.");
process.exit(1);
}
}