mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(release): harden plugin package preflight
This commit is contained in:
@@ -321,6 +321,7 @@ Upgrade with the beta channel.
|
||||
Before tagging or publishing, run:
|
||||
|
||||
```bash
|
||||
pnpm release:fast-pretag-check
|
||||
pnpm check:architecture
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
@@ -329,6 +330,21 @@ pnpm release:check
|
||||
pnpm test:install:smoke
|
||||
```
|
||||
|
||||
- Treat `pnpm release:fast-pretag-check` as a hard packaging gate. Every
|
||||
publishable plugin must have a non-empty package-root `README.md`, build its
|
||||
package-local runtime, and pass the npm and ClawHub release metadata checks
|
||||
before a tag or publish workflow can start. Do not defer README, entrypoint,
|
||||
or packed-artifact failures to postpublish verification.
|
||||
- Before tagging, require green CI for the exact release-candidate SHA, not an
|
||||
earlier branch SHA. Heal every related red CI, release-check, packaging, or
|
||||
root-Dockerfile lane on the release branch, forward-port the fix to `main`,
|
||||
and rerun the affected exact-SHA gates. Never waive a red Docker lane because
|
||||
npm preflight passed.
|
||||
- Root Dockerfile proof is mandatory before every beta and stable tag. Run the
|
||||
release `install-smoke` group or equivalent root Dockerfile build for the
|
||||
exact candidate SHA and require it to pass. The tag-triggered Docker Release
|
||||
workflow is post-tag publishing, not the first valid proof that the root
|
||||
Dockerfile can build.
|
||||
- Before tagging, diff publishable plugin package manifests against the last
|
||||
reachable stable/beta release tag. For every newly publishable package
|
||||
(`openclaw.release.publishToNpm: true` or `publishToClawHub: true`) whose
|
||||
@@ -644,9 +660,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
off, live OpenAI off, and regression failure off. Let it run in parallel
|
||||
with preflight and validation work.
|
||||
10. Run the fast local beta preflight from the release branch before any npm
|
||||
preflight or publish. Keep expensive Docker, Parallels, and published-package
|
||||
install/update lanes for after the beta is live unless the operator asks to
|
||||
run them before beta publication.
|
||||
preflight or publish. Require exact-SHA CI and root Dockerfile install-smoke
|
||||
to be green before tagging. Keep the remaining expensive Docker, Parallels,
|
||||
and published-package install/update lanes for after the beta is live unless
|
||||
the operator asks to run them before beta publication.
|
||||
11. For beta releases, skip mac app build/sign/notarize unless beta scope or a
|
||||
release blocker specifically requires it. For stable releases, include the
|
||||
mac app, signing, notarization, and appcast path.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# @openclaw/llama-cpp-provider
|
||||
|
||||
Official llama.cpp embedding provider for OpenClaw.
|
||||
|
||||
This plugin runs local GGUF embedding models through `node-llama-cpp`.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/llama-cpp-provider
|
||||
```
|
||||
|
||||
Restart the Gateway after installing or updating the plugin. Use Node 24 for
|
||||
native installs and updates.
|
||||
|
||||
## Configure
|
||||
|
||||
Set `agents.defaults.memorySearch.provider` to `local`. By default, the plugin
|
||||
downloads and uses the EmbeddingGemma GGUF model. Configure
|
||||
`agents.defaults.memorySearch.local.modelPath` to use another local path, Hugging
|
||||
Face model URI, or HTTPS model URL.
|
||||
|
||||
## Package
|
||||
|
||||
- Plugin id: `llama-cpp`
|
||||
- Package: `@openclaw/llama-cpp-provider`
|
||||
- Minimum OpenClaw host: `2026.6.2`
|
||||
@@ -154,11 +154,7 @@ export function collectClawHubPublishablePluginPackages(
|
||||
continue;
|
||||
}
|
||||
|
||||
const errors = collectPublishablePluginPackageErrors({
|
||||
extensionId,
|
||||
packageDir,
|
||||
packageJson,
|
||||
});
|
||||
const errors = collectPublishablePluginPackageErrors(candidate);
|
||||
if (errors.length > 0) {
|
||||
validationErrors.push(...errors.map((error) => `${extensionId}: ${error}`));
|
||||
continue;
|
||||
|
||||
@@ -444,7 +444,12 @@ export function resolveAugmentedPluginNpmPackageJson(params) {
|
||||
openclaw: {
|
||||
...plan.packageJson.openclaw,
|
||||
runtimeExtensions: plan.runtimeExtensions,
|
||||
...(plan.runtimeSetupEntry ? { runtimeSetupEntry: plan.runtimeSetupEntry } : {}),
|
||||
...(plan.runtimeSetupEntry
|
||||
? {
|
||||
setupEntry: plan.runtimeSetupEntry,
|
||||
runtimeSetupEntry: plan.runtimeSetupEntry,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
if (shouldBundleDependencies(params.bundleDependencies, plan.packageJson)) {
|
||||
|
||||
@@ -80,6 +80,7 @@ export type PublishablePluginPackageCandidate<
|
||||
extensionId: string;
|
||||
packageDir: string;
|
||||
packageJson: TPackageJson;
|
||||
readmeText?: string;
|
||||
};
|
||||
|
||||
export const OPENCLAW_PLUGIN_NPM_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
|
||||
@@ -88,6 +89,14 @@ function readPluginPackageJson(path: string): unknown {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
function readOptionalTextFile(path: string): string | undefined {
|
||||
try {
|
||||
return readFileSync(path, "utf8");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function collectExtensionPackageJsonCandidates<
|
||||
TPackageJson extends PluginPackageJson = PluginPackageJson,
|
||||
>(rootDir = resolve(".")): PublishablePluginPackageCandidate<TPackageJson>[] {
|
||||
@@ -106,6 +115,7 @@ export function collectExtensionPackageJsonCandidates<
|
||||
extensionId: dir.name,
|
||||
packageDir,
|
||||
packageJson: readPluginPackageJson(packageJsonPath) as TPackageJson,
|
||||
readmeText: readOptionalTextFile(join(absolutePackageDir, "README.md")),
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
@@ -242,6 +252,9 @@ export function collectPublishablePluginPackageErrors(
|
||||
if (packageJson.private === true) {
|
||||
errors.push("package.json private must not be true.");
|
||||
}
|
||||
if (!candidate.readmeText?.trim()) {
|
||||
errors.push("README.md must exist and contain package documentation.");
|
||||
}
|
||||
if (repositoryUrl !== OPENCLAW_PLUGIN_NPM_REPOSITORY_URL) {
|
||||
errors.push(
|
||||
`package.json repository.url must be "${OPENCLAW_PLUGIN_NPM_REPOSITORY_URL}" so npm provenance can validate GitHub trusted publishing; found "${repositoryUrl || "<missing>"}".`,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { collectClawHubPublishablePluginPackages } from "./lib/plugin-clawhub-release.ts";
|
||||
import { collectPublishablePluginPackages } from "./lib/plugin-npm-release.ts";
|
||||
|
||||
const DEFAULT_CLAWHUB_CLI_PACKAGE = "clawhub@0.21.0";
|
||||
|
||||
type PluginReleasePretagPackTarget = {
|
||||
packageDir: string;
|
||||
packageName: string;
|
||||
packClawHub: boolean;
|
||||
packNpm: boolean;
|
||||
};
|
||||
|
||||
export function collectPluginReleasePretagPackTargets(
|
||||
rootDir = resolve("."),
|
||||
): PluginReleasePretagPackTarget[] {
|
||||
const targets = new Map<string, PluginReleasePretagPackTarget>();
|
||||
|
||||
for (const plugin of collectPublishablePluginPackages(rootDir)) {
|
||||
targets.set(plugin.packageDir, {
|
||||
packageDir: plugin.packageDir,
|
||||
packageName: plugin.packageName,
|
||||
packClawHub: false,
|
||||
packNpm: true,
|
||||
});
|
||||
}
|
||||
for (const plugin of collectClawHubPublishablePluginPackages(rootDir)) {
|
||||
const existing = targets.get(plugin.packageDir);
|
||||
targets.set(plugin.packageDir, {
|
||||
packageDir: plugin.packageDir,
|
||||
packageName: plugin.packageName,
|
||||
packClawHub: true,
|
||||
packNpm: existing?.packNpm ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
return [...targets.values()].toSorted((left, right) =>
|
||||
left.packageName.localeCompare(right.packageName),
|
||||
);
|
||||
}
|
||||
|
||||
function runCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
params: { cwd: string; env?: NodeJS.ProcessEnv; quietStdout?: boolean },
|
||||
) {
|
||||
execFileSync(command, args, {
|
||||
cwd: params.cwd,
|
||||
env: params.env ?? process.env,
|
||||
stdio: params.quietStdout ? ["inherit", "ignore", "inherit"] : "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
export function runPluginReleasePretagPackCheck(rootDir = resolve(".")) {
|
||||
const targets = collectPluginReleasePretagPackTargets(rootDir);
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-plugin-pretag-pack-"));
|
||||
const wrapperDir = join(tempRoot, "bin");
|
||||
mkdirSync(wrapperDir);
|
||||
const clawHubWrapper = join(wrapperDir, "clawhub");
|
||||
writeFileSync(
|
||||
clawHubWrapper,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'exec npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- clawhub "$@"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(clawHubWrapper, 0o755);
|
||||
|
||||
try {
|
||||
runCommand(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/check-plugin-npm-runtime-builds.mjs",
|
||||
...targets.flatMap((target) => ["--package", target.packageDir]),
|
||||
],
|
||||
{
|
||||
cwd: rootDir,
|
||||
},
|
||||
);
|
||||
|
||||
const packEnv = {
|
||||
...process.env,
|
||||
CLAWHUB_CLI_PACKAGE: process.env.CLAWHUB_CLI_PACKAGE?.trim() || DEFAULT_CLAWHUB_CLI_PACKAGE,
|
||||
OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD: "0",
|
||||
PATH: `${wrapperDir}:${process.env.PATH ?? ""}`,
|
||||
};
|
||||
for (const [index, target] of targets.entries()) {
|
||||
if (target.packNpm) {
|
||||
console.log(`npm pack: ${target.packageName}`);
|
||||
runCommand("bash", ["scripts/plugin-npm-publish.sh", "--pack-dry-run", target.packageDir], {
|
||||
cwd: rootDir,
|
||||
env: packEnv,
|
||||
quietStdout: true,
|
||||
});
|
||||
}
|
||||
if (target.packClawHub) {
|
||||
const outputDir = join(tempRoot, `clawhub-${index}`);
|
||||
console.log(`ClawHub pack: ${target.packageName}`);
|
||||
runCommand("bash", ["scripts/plugin-clawhub-publish.sh", "--pack", target.packageDir], {
|
||||
cwd: rootDir,
|
||||
env: {
|
||||
...packEnv,
|
||||
OPENCLAW_CLAWHUB_PACK_OUTPUT_DIR: outputDir,
|
||||
},
|
||||
quietStdout: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log(`plugin-release-pretag-pack-check: packed ${targets.length} publishable plugins.`);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
runPluginReleasePretagPackCheck();
|
||||
}
|
||||
@@ -12,7 +12,7 @@ pnpm plugins:sync:check
|
||||
pnpm release:generated:check
|
||||
pnpm release:plugins:npm:check -- --selection-mode all-publishable
|
||||
pnpm release:plugins:clawhub:check -- --selection-mode all-publishable
|
||||
node scripts/check-plugin-npm-runtime-builds.mjs --package extensions/diffs-language-pack
|
||||
node --import tsx scripts/plugin-release-pretag-pack-check.ts
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
pnpm release:openclaw:npm:check
|
||||
|
||||
@@ -1150,6 +1150,7 @@ function createTempPluginRepo(
|
||||
join(repoDir, "extensions", currentExtensionId, "index.ts"),
|
||||
`export const ${currentExtensionId.replaceAll(/[-.]/g, "_")} = 1;\n`,
|
||||
);
|
||||
writeFileSync(join(repoDir, "extensions", currentExtensionId, "README.md"), "# Demo plugin\n");
|
||||
}
|
||||
|
||||
git(repoDir, ["init", "-b", "main"]);
|
||||
|
||||
@@ -275,7 +275,7 @@ describe("plugin npm package manifest staging", () => {
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
setupEntry: "./setup-entry.ts",
|
||||
setupEntry: "./dist/setup-entry.js",
|
||||
compat: {
|
||||
pluginApi: ">=2026.4.30",
|
||||
},
|
||||
@@ -296,6 +296,7 @@ describe("plugin npm package manifest staging", () => {
|
||||
);
|
||||
expect(stagedPackageJson.openclaw.extensions).toEqual(["./index.ts"]);
|
||||
expect(stagedPackageJson.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
|
||||
expect(stagedPackageJson.openclaw.setupEntry).toBe("./dist/setup-entry.js");
|
||||
expect(stagedPackageJson.openclaw.runtimeSetupEntry).toBe("./dist/setup-entry.js");
|
||||
expect(stagedPackageJson.bundledDependencies).toEqual([]);
|
||||
expect(stagedPackageJson.bundleDependencies).toBeUndefined();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Plugin npm release tests validate plugin npm release artifacts.
|
||||
import { mkdirSync, readFileSync } from "node:fs";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { bundledPluginFile, bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
@@ -98,12 +98,19 @@ function externalPluginContract(version: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function writePluginReadme(repoDir: string, extensionId: string): void {
|
||||
const packageDir = join(repoDir, "extensions", extensionId);
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
writeFileSync(join(packageDir, "README.md"), `# ${extensionId}\n`);
|
||||
}
|
||||
|
||||
describe("collectPublishablePluginPackageErrors", () => {
|
||||
it("accepts a valid publishable plugin package candidate", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "zalo",
|
||||
packageDir: bundledPluginRoot("zalo"),
|
||||
readmeText: "# Zalo\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/zalo",
|
||||
version: "2026.3.15",
|
||||
@@ -131,6 +138,7 @@ describe("collectPublishablePluginPackageErrors", () => {
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "broken",
|
||||
packageDir: bundledPluginRoot("broken"),
|
||||
readmeText: "# Broken\n",
|
||||
packageJson: {
|
||||
name: "broken",
|
||||
version: "latest",
|
||||
@@ -162,6 +170,7 @@ describe("collectPublishablePluginPackageErrors", () => {
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "twitch",
|
||||
packageDir: bundledPluginRoot("twitch"),
|
||||
readmeText: "# Twitch\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/twitch",
|
||||
version: "2026.5.1-beta.1",
|
||||
@@ -187,6 +196,7 @@ describe("collectPublishablePluginPackageErrors", () => {
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "voice-call",
|
||||
packageDir: bundledPluginRoot("voice-call"),
|
||||
readmeText: "# Voice call\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/voice-call",
|
||||
version: "2026.5.1-beta.1",
|
||||
@@ -211,6 +221,7 @@ describe("collectPublishablePluginPackageErrors", () => {
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "voice-call",
|
||||
packageDir: bundledPluginRoot("voice-call"),
|
||||
readmeText: "# Voice call\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/voice-call",
|
||||
version: "2026.5.1-beta.1",
|
||||
@@ -234,6 +245,34 @@ describe("collectPublishablePluginPackageErrors", () => {
|
||||
"openclaw.build.openclawVersion is required for external code plugin packages.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires package documentation before publishing", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "zalo",
|
||||
packageDir: bundledPluginRoot("zalo"),
|
||||
readmeText: " \n",
|
||||
packageJson: {
|
||||
name: "@openclaw/zalo",
|
||||
version: "2026.3.15",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.3.15"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/zalo",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual(["README.md must exist and contain package documentation."]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPluginReleaseVersionFloorErrors", () => {
|
||||
@@ -291,6 +330,7 @@ describe("collectPublishablePluginPackages", () => {
|
||||
it("collects publishable npm plugins from extension package manifests", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
|
||||
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });
|
||||
writePluginReadme(repoDir, "demo-plugin");
|
||||
writeJsonFile(join(repoDir, "extensions", "demo-plugin", "package.json"), {
|
||||
name: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10",
|
||||
@@ -326,6 +366,7 @@ describe("collectPublishablePluginPackages", () => {
|
||||
it("does not validate unselected publishable plugin manifests", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
|
||||
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });
|
||||
writePluginReadme(repoDir, "demo-plugin");
|
||||
writeJsonFile(join(repoDir, "extensions", "demo-plugin", "package.json"), {
|
||||
name: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10-beta.1",
|
||||
@@ -404,6 +445,7 @@ describe("collectPublishablePluginPackages", () => {
|
||||
it("publishes alpha plugin packages to the alpha dist-tag", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
|
||||
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });
|
||||
writePluginReadme(repoDir, "demo-plugin");
|
||||
writeJsonFile(join(repoDir, "extensions", "demo-plugin", "package.json"), {
|
||||
name: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10-alpha.1",
|
||||
|
||||
@@ -1695,6 +1695,11 @@ describe("package artifact reuse", () => {
|
||||
const clawHubNewWorkflow = readFileSync(".github/workflows/plugin-clawhub-new.yml", "utf8");
|
||||
const pluginNpmWorkflow = readFileSync(".github/workflows/plugin-npm-release.yml", "utf8");
|
||||
const openclawNpmWorkflow = readFileSync(".github/workflows/openclaw-npm-release.yml", "utf8");
|
||||
const fastPretagScript = readFileSync("scripts/release-fast-pretag-check.sh", "utf8");
|
||||
const pluginPretagPackScript = readFileSync(
|
||||
"scripts/plugin-release-pretag-pack-check.ts",
|
||||
"utf8",
|
||||
);
|
||||
const approvalScript = readFileSync("scripts/validate-release-publish-approval.mjs", "utf8");
|
||||
const clawHubReleasePlanScript = readFileSync(
|
||||
"scripts/lib/openclaw-release-clawhub-plan.ts",
|
||||
@@ -1721,6 +1726,15 @@ describe("package artifact reuse", () => {
|
||||
expect(packageJson.scripts?.["release:fast-pretag-check"]).toBe(
|
||||
"bash scripts/release-fast-pretag-check.sh",
|
||||
);
|
||||
expect(fastPretagScript).toContain(
|
||||
"node --import tsx scripts/plugin-release-pretag-pack-check.ts",
|
||||
);
|
||||
expect(fastPretagScript).not.toContain(
|
||||
"check-plugin-npm-runtime-builds.mjs --package extensions/diffs-language-pack",
|
||||
);
|
||||
expect(pluginPretagPackScript).toContain("scripts/check-plugin-npm-runtime-builds.mjs");
|
||||
expect(pluginPretagPackScript).toContain("scripts/plugin-npm-publish.sh");
|
||||
expect(pluginPretagPackScript).toContain("scripts/plugin-clawhub-publish.sh");
|
||||
expect(clawHubWorkflow).toContain('CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"');
|
||||
expect(clawHubWorkflow).not.toContain("CLAWHUB_REPOSITORY:");
|
||||
expect(clawHubWorkflow).not.toContain("CLAWHUB_REF:");
|
||||
|
||||
Reference in New Issue
Block a user