chore: add release prepare command

This commit is contained in:
Gianfranco Piana
2026-04-26 12:40:42 -03:00
parent 6169c6445b
commit ed0757ab80
5 changed files with 211 additions and 20 deletions
+1 -1
View File
@@ -132,7 +132,7 @@ npm run smoke:openclaw-host -- /absolute/path/to/openclaw
npm run smoke:registry-openclaw-host -- <published-version> /absolute/path/to/openclaw
```
Release instructions, including GitHub Actions trusted publishing with npm provenance, live in [`RELEASING.md`](RELEASING.md).
Release instructions, including `npm run release:prepare -- <version> --host /absolute/path/to/openclaw` and GitHub Actions trusted publishing with npm provenance, live in [`RELEASING.md`](RELEASING.md).
The local test shim supports typechecking and tests without a full OpenClaw host checkout. Runtime behavior depends on a real OpenClaw host, so run the host smoke against a current checkout before release.
+16 -19
View File
@@ -11,32 +11,29 @@
## Release
1. Update the package version in `package.json`, then sync generated metadata:
1. Prepare the version bump from a clean branch:
```bash
npm run sync:release-metadata
npm run release:prepare -- <version> --host /absolute/path/to/openclaw
```
If you change the minimum supported OpenClaw version, keep
`openclaw.install`, `openclaw.compat`, and `openclaw.build` aligned too.
This updates `package.json`, `openclaw.plugin.json`, and `package-lock.json`,
syncs generated metadata, checks the matching `v<version>` tag, runs
`release:verify`, and smoke-tests against the supplied OpenClaw checkout.
2. Run the release checks:
```bash
npm install
npm run release:verify
```
CI runs the same release verification, and `prepublishOnly` runs it again
before any npm publish.
3. Smoke-test against a current local OpenClaw checkout:
If you do not have a host checkout available, omit `--host` and run the host
smoke before publishing:
```bash
npm run smoke:openclaw-host -- /absolute/path/to/openclaw
```
4. Create and publish the matching GitHub release/tag:
If you change the minimum supported OpenClaw version, keep
`openclaw.install`, `openclaw.compat`, and `openclaw.build` aligned too.
2. Open and merge a PR with the version and metadata changes.
3. Create and publish the matching GitHub release/tag from `main`:
```bash
npm run release:check-tag -- v<version>
@@ -46,7 +43,7 @@
Publishing the GitHub release triggers `.github/workflows/npm-publish.yml`,
which publishes the package to npm with provenance through GitHub OIDC.
5. Watch the publish workflow and verify npm:
4. Watch the publish workflow and verify npm:
```bash
gh run list --workflow npm-publish.yml --limit 1
@@ -54,13 +51,13 @@
npm view @gianfrancopiana/openclaw-autoresearch@<version> version
```
6. Verify the published registry tarball against the same host:
5. Verify the published registry tarball against the same host:
```bash
npm run smoke:registry-openclaw-host -- <published-version> /absolute/path/to/openclaw
```
7. Verify install:
6. Verify install:
```bash
openclaw plugins install @gianfrancopiana/openclaw-autoresearch
+1
View File
@@ -12,6 +12,7 @@
"validate": "npm run check:release-metadata && npm run typecheck && npm run test",
"release:verify": "npm run validate && npm pack --dry-run",
"release:check-tag": "node ./scripts/check-release-tag.mjs",
"release:prepare": "node ./scripts/prepare-release.mjs",
"prepublishOnly": "npm run release:verify",
"smoke:openclaw-host": "node ./scripts/smoke-openclaw-host.mjs",
"smoke:registry-openclaw-host": "node ./scripts/smoke-openclaw-registry.mjs"
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
const usage = [
"Usage: npm run release:prepare -- <version> [--host /path/to/openclaw] [--skip-verify]",
"",
"Bumps package/plugin metadata, refreshes the lockfile, runs release checks, and prints next steps.",
].join("\n");
function fail(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}
function readJson(file) {
return JSON.parse(readFileSync(file, "utf8"));
}
function writeJson(file, value) {
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}
function parseArgs(argv) {
const options = {
version: undefined,
host: undefined,
repoRoot: defaultRepoRoot,
skipVerify: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--host") {
options.host = argv[index + 1];
index += 1;
} else if (arg === "--repo") {
options.repoRoot = path.resolve(argv[index + 1] ?? "");
index += 1;
} else if (arg === "--skip-verify") {
options.skipVerify = true;
} else if (!options.version) {
options.version = arg;
} else {
fail(`${usage}\n\nUnexpected argument: ${arg}`);
}
}
if (!options.version || !versionPattern.test(options.version)) {
fail(usage);
}
if (options.host === undefined && argv.includes("--host")) {
fail(`${usage}\n\n--host requires a path`);
}
return options;
}
function run(command, args, cwd) {
const result = spawnSync(command, args, {
cwd,
encoding: "utf8",
env: process.env,
maxBuffer: 16 * 1024 * 1024,
});
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
fail(`${command} ${args.join(" ")} failed with exit ${result.status}`);
}
}
function prepareRelease(options) {
const repoRoot = path.resolve(options.repoRoot);
const packagePath = path.join(repoRoot, "package.json");
const manifestPath = path.join(repoRoot, "openclaw.plugin.json");
const packageJson = readJson(packagePath);
const manifest = readJson(manifestPath);
packageJson.version = options.version;
manifest.version = options.version;
writeJson(packagePath, packageJson);
writeJson(manifestPath, manifest);
run("npm", ["run", "sync:release-metadata"], repoRoot);
run("npm", ["install", "--package-lock-only"], repoRoot);
run("npm", ["run", "release:check-tag", "--", `v${options.version}`], repoRoot);
if (!options.skipVerify) {
run("npm", ["run", "release:verify"], repoRoot);
}
if (options.host) {
run("npm", ["run", "smoke:openclaw-host", "--", options.host], repoRoot);
}
process.stdout.write(`\nRelease prep complete for v${options.version}\n`);
process.stdout.write("Next steps:\n");
process.stdout.write("- Review the git diff\n");
process.stdout.write("- Open and merge a PR with the version/metadata changes\n");
process.stdout.write(`- After merge, create the GitHub release: gh release create v${options.version} --target main --title v${options.version} --notes-file release-notes.md\n`);
}
try {
prepareRelease(parseArgs(process.argv.slice(2)));
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
+71
View File
@@ -0,0 +1,71 @@
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const script = fileURLToPath(new URL("../scripts/prepare-release.mjs", import.meta.url));
function writeJson(file: string, value: unknown) {
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}
describe("release prepare command", () => {
it("bumps metadata, refreshes the lockfile, and prints release next steps", () => {
const repo = mkdtempSync(path.join(tmpdir(), "openclaw-autoresearch-release-prep-test-"));
const log = path.join(repo, "commands.log");
writeJson(path.join(repo, "package.json"), {
name: "@gianfrancopiana/openclaw-autoresearch",
version: "1.0.8",
type: "module",
scripts: {
"sync:release-metadata": "node ./sync.mjs",
"release:check-tag": "node ./check-tag.mjs",
"release:verify": "node ./verify.mjs",
"smoke:openclaw-host": "node ./smoke.mjs",
},
});
writeJson(path.join(repo, "openclaw.plugin.json"), {
name: "openclaw-autoresearch",
version: "1.0.8",
});
writeFileSync(
path.join(repo, "sync.mjs"),
`import { readFileSync, writeFileSync, appendFileSync } from 'node:fs';\nconst pkg = JSON.parse(readFileSync('package.json', 'utf8'));\nconst manifest = JSON.parse(readFileSync('openclaw.plugin.json', 'utf8'));\nmanifest.version = pkg.version;\nwriteFileSync('openclaw.plugin.json', JSON.stringify(manifest, null, 2) + '\\n');\nappendFileSync(${JSON.stringify(log)}, 'sync\\n');\n`,
);
writeFileSync(
path.join(repo, "check-tag.mjs"),
`import { appendFileSync } from 'node:fs';\nappendFileSync(${JSON.stringify(log)}, 'check ' + process.argv.at(-1) + '\\n');\n`,
);
writeFileSync(
path.join(repo, "verify.mjs"),
`import { appendFileSync } from 'node:fs';\nappendFileSync(${JSON.stringify(log)}, 'verify\\n');\n`,
);
writeFileSync(
path.join(repo, "smoke.mjs"),
`import { appendFileSync } from 'node:fs';\nappendFileSync(${JSON.stringify(log)}, 'smoke ' + process.argv.at(-1) + '\\n');\n`,
);
const result = spawnSync(
process.execPath,
[script, "2.0.0", "--repo", repo, "--skip-verify", "--host", "/tmp/openclaw-host"],
{ encoding: "utf8" },
);
expect(result.status).toBe(0);
expect(JSON.parse(readFileSync(path.join(repo, "package.json"), "utf8")).version).toBe("2.0.0");
expect(JSON.parse(readFileSync(path.join(repo, "openclaw.plugin.json"), "utf8")).version).toBe("2.0.0");
expect(readFileSync(log, "utf8")).toContain("sync\ncheck v2.0.0\nsmoke /tmp/openclaw-host\n");
expect(result.stdout).toContain("Release prep complete for v2.0.0");
expect(result.stdout).toContain("gh release create v2.0.0");
});
it("rejects versions that are not plain semver", () => {
const result = spawnSync(process.execPath, [script, "not-a-version"], { encoding: "utf8" });
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("Usage: npm run release:prepare -- <version>");
});
});