204 lines
5.5 KiB
JavaScript
204 lines
5.5 KiB
JavaScript
#!/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}`);
|
|
}
|
|
}
|