chore(release): add patch release plan
This commit is contained in:
parent
bdef58eeb9
commit
527b2eae82
@ -7,11 +7,13 @@
|
||||
- Add grouped root facades: `pluginRoot`, `fixtureSuites`, `staticInspection`, `reports`, `contracts`, `ci`, `runtime`, and `synthetic`.
|
||||
- Expose contract capture, contract coverage, CI rollup, runtime profile, ref/profile diff, import-loop, and synthetic probe helpers from the root package API.
|
||||
- Add a release follow-through guard that fails when Crabpot scripts regress to the legacy `advanced.js` bundle.
|
||||
- Add a package-contents release guard for npm tarball entrypoints, examples, and README assets.
|
||||
|
||||
### Changed
|
||||
|
||||
- Move Crabpot integration scripts to the root public API while keeping Crabpot as the fixture corpus and report consumer.
|
||||
- Keep generic artifact writing out of the root API; Crabpot-owned runner scripts write their own JSON outputs.
|
||||
- Ship the README banner asset in the npm package so the published README does not reference a missing local image.
|
||||
- Document the grouped root import path for embedding harnesses without turning the README into a full API dump.
|
||||
|
||||
## 0.3.0 - 2026-04-27
|
||||
|
||||
@ -81,6 +81,15 @@ Draft release notes from the current `Unreleased` section with:
|
||||
npm run release:notes
|
||||
```
|
||||
|
||||
Preview the exact patch-release edits without changing files:
|
||||
|
||||
```bash
|
||||
npm run release:plan
|
||||
```
|
||||
|
||||
That prints the next patch version, release tag, changelog heading, Crabpot
|
||||
source ref, and post-publish Crabpot package pin.
|
||||
|
||||
## Crabpot follow-through
|
||||
|
||||
Before tagging a release, update Crabpot's `pluginInspectorRef` to the
|
||||
|
||||
@ -52,6 +52,7 @@
|
||||
"check": "npm test && npm run release:contents",
|
||||
"release:crabpot": "node scripts/check-crabpot-followthrough.mjs",
|
||||
"release:contents": "node scripts/check-package-contents.mjs",
|
||||
"release:plan": "node scripts/release-plan.mjs",
|
||||
"release:readiness": "npm run release:local && npm run release:crabpot",
|
||||
"release:local": "npm run check",
|
||||
"release:notes": "node scripts/release-notes.mjs --unreleased",
|
||||
|
||||
203
scripts/release-plan.mjs
Normal file
203
scripts/release-plan.mjs
Normal file
@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { extractReleaseNotes } from "./release-notes.mjs";
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const root = path.resolve(options.root);
|
||||
const packageJson = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
||||
const changelogText = readFileSync(path.join(root, "CHANGELOG.md"), "utf8");
|
||||
const plan = buildReleasePlan({
|
||||
packageName: packageJson.name,
|
||||
packageVersion: packageJson.version,
|
||||
changelogText,
|
||||
releaseRef: options.releaseRef ?? gitHead(root),
|
||||
releaseDate: options.date,
|
||||
nextVersion: options.version,
|
||||
});
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(plan, null, 2));
|
||||
} else {
|
||||
printReleasePlan(plan);
|
||||
}
|
||||
|
||||
if (plan.status === "fail") {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildReleasePlan({
|
||||
packageName = "@openclaw/plugin-inspector",
|
||||
packageVersion,
|
||||
changelogText,
|
||||
releaseRef,
|
||||
releaseDate = today(),
|
||||
nextVersion,
|
||||
}) {
|
||||
const version = nextVersion ?? bumpPatch(packageVersion);
|
||||
const checks = [
|
||||
{
|
||||
id: "version-advance",
|
||||
status: compareVersions(version, packageVersion) > 0 ? "pass" : "fail",
|
||||
message: `${version} is newer than ${packageVersion}`,
|
||||
expected: `>${packageVersion}`,
|
||||
actual: version,
|
||||
},
|
||||
{
|
||||
id: "changelog-unreleased",
|
||||
status: hasUnreleasedNotes(changelogText) ? "pass" : "fail",
|
||||
message: "CHANGELOG.md has non-empty Unreleased notes",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
status: checks.some((check) => check.status === "fail") ? "fail" : "pass",
|
||||
packageName,
|
||||
currentVersion: packageVersion,
|
||||
nextVersion: version,
|
||||
releaseDate,
|
||||
releaseRef,
|
||||
tagName: `v${version}`,
|
||||
changelogHeading: `## ${version} - ${releaseDate}`,
|
||||
crabpotSourceRef: releaseRef,
|
||||
crabpotPackagePin: `${packageName}@${version}`,
|
||||
releaseNotes: safeReleaseNotes(changelogText),
|
||||
checks,
|
||||
steps: [
|
||||
`update package.json version to ${version}`,
|
||||
`move CHANGELOG.md Unreleased notes to "${`## ${version} - ${releaseDate}`}"`,
|
||||
`set crabpot pluginInspectorRef to ${releaseRef}`,
|
||||
"run npm run release:readiness",
|
||||
`create and push annotated tag ${`v${version}`}`,
|
||||
`after npm publish, set crabpot pluginInspectorPackage to ${packageName}@${version}`,
|
||||
"run npm run release:crabpot -- --published",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function hasUnreleasedNotes(changelogText) {
|
||||
try {
|
||||
return extractReleaseNotes({ changelogText, version: "Unreleased" })
|
||||
.split(/\r?\n/)
|
||||
.some((line) => line.trim().startsWith("- "));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function safeReleaseNotes(changelogText) {
|
||||
try {
|
||||
return extractReleaseNotes({ changelogText, version: "Unreleased" });
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function bumpPatch(version) {
|
||||
const match = version?.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!match) {
|
||||
throw new Error(`cannot infer next patch version from ${version}`);
|
||||
}
|
||||
return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
|
||||
}
|
||||
|
||||
function compareVersions(left, right) {
|
||||
const leftParts = parseVersion(left);
|
||||
const rightParts = parseVersion(right);
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (leftParts[index] !== rightParts[index]) {
|
||||
return leftParts[index] - rightParts[index];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parseVersion(version) {
|
||||
const match = version?.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!match) {
|
||||
throw new Error(`invalid semver version: ${version}`);
|
||||
}
|
||||
return match.slice(1).map(Number);
|
||||
}
|
||||
|
||||
function today() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function gitHead(root) {
|
||||
return execFileSync("git", ["rev-parse", "HEAD"], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
root: ".",
|
||||
date: today(),
|
||||
json: false,
|
||||
releaseRef: undefined,
|
||||
version: undefined,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--root") {
|
||||
options.root = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--date") {
|
||||
options.date = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--json") {
|
||||
options.json = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--ref") {
|
||||
options.releaseRef = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--version") {
|
||||
options.version = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (!options.version) {
|
||||
options.version = arg;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function printReleasePlan(plan) {
|
||||
console.log(`release plan: ${plan.status}`);
|
||||
console.log(`package: ${plan.packageName}`);
|
||||
console.log(`current version: ${plan.currentVersion}`);
|
||||
console.log(`next version: ${plan.nextVersion}`);
|
||||
console.log(`release date: ${plan.releaseDate}`);
|
||||
console.log(`release ref: ${plan.releaseRef}`);
|
||||
console.log(`tag: ${plan.tagName}`);
|
||||
for (const check of plan.checks) {
|
||||
console.log(`- ${check.status.toUpperCase()} ${check.id}: ${check.message}`);
|
||||
if (check.status === "fail" && check.actual !== check.expected) {
|
||||
console.log(` expected: ${check.expected}`);
|
||||
console.log(` actual: ${check.actual}`);
|
||||
}
|
||||
}
|
||||
console.log("steps:");
|
||||
for (const step of plan.steps) {
|
||||
console.log(`- ${step}`);
|
||||
}
|
||||
}
|
||||
57
test/release-plan.test.js
Normal file
57
test/release-plan.test.js
Normal file
@ -0,0 +1,57 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { buildReleasePlan } from "../scripts/release-plan.mjs";
|
||||
|
||||
const changelog = [
|
||||
"# Changelog",
|
||||
"",
|
||||
"## Unreleased",
|
||||
"",
|
||||
"### Changed",
|
||||
"",
|
||||
"- Tighten package contents.",
|
||||
"",
|
||||
"## 0.3.0 - 2026-04-27",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
test("release plan infers the next patch version", () => {
|
||||
const plan = buildReleasePlan({
|
||||
packageVersion: "0.3.0",
|
||||
changelogText: changelog,
|
||||
releaseDate: "2026-04-28",
|
||||
releaseRef: "abc123",
|
||||
});
|
||||
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.equal(plan.nextVersion, "0.3.1");
|
||||
assert.equal(plan.tagName, "v0.3.1");
|
||||
assert.equal(plan.changelogHeading, "## 0.3.1 - 2026-04-28");
|
||||
assert.equal(plan.crabpotSourceRef, "abc123");
|
||||
assert.equal(plan.crabpotPackagePin, "@openclaw/plugin-inspector@0.3.1");
|
||||
});
|
||||
|
||||
test("release plan rejects non-advancing versions", () => {
|
||||
const plan = buildReleasePlan({
|
||||
packageVersion: "0.3.0",
|
||||
nextVersion: "0.3.0",
|
||||
changelogText: changelog,
|
||||
releaseDate: "2026-04-28",
|
||||
releaseRef: "abc123",
|
||||
});
|
||||
|
||||
assert.equal(plan.status, "fail");
|
||||
assert.equal(plan.checks.find((check) => check.id === "version-advance").status, "fail");
|
||||
});
|
||||
|
||||
test("release plan requires unreleased changelog bullets", () => {
|
||||
const plan = buildReleasePlan({
|
||||
packageVersion: "0.3.0",
|
||||
changelogText: "# Changelog\n\n## Unreleased\n",
|
||||
releaseDate: "2026-04-28",
|
||||
releaseRef: "abc123",
|
||||
});
|
||||
|
||||
assert.equal(plan.status, "fail");
|
||||
assert.equal(plan.checks.find((check) => check.id === "changelog-unreleased").status, "fail");
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user