Files
openwork/scripts/release/versions.mjs
T

104 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Stable-version helpers for the tag-driven release flow.
*
* Git tags (vX.Y.Z) are the single source of truth for released versions —
* package.json files hold a permanent "0.0.0-dev" placeholder and CI stamps
* the real version into the workspace at build time (see stamp-version.mjs).
*
* CLI:
* node scripts/release/versions.mjs latest # highest stable tag version
* node scripts/release/versions.mjs next --bump patch # next version from tags
*/
import { execFileSync } from "node:child_process";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
export const STABLE_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
export function parseStableVersion(value) {
const match = typeof value === "string" ? value.match(STABLE_VERSION_PATTERN) : null;
return match ? match.slice(1).map(Number) : null;
}
export function compareStableVersions(left, right) {
const leftParts = parseStableVersion(left);
const rightParts = parseStableVersion(right);
if (!leftParts || !rightParts) {
throw new Error(`Cannot compare non-stable versions: ${left} vs ${right}`);
}
for (let index = 0; index < 3; index += 1) {
const difference = leftParts[index] - rightParts[index];
if (difference !== 0) return difference;
}
return 0;
}
export function stableVersionFromTag(tag) {
const trimmed = typeof tag === "string" ? tag.trim() : "";
if (!trimmed.startsWith("v")) return null;
const version = trimmed.slice(1);
return parseStableVersion(version) ? version : null;
}
export function highestStableVersion(versions) {
let highest = null;
for (const version of versions) {
if (!parseStableVersion(version)) continue;
if (highest === null || compareStableVersions(version, highest) > 0) {
highest = version;
}
}
return highest;
}
export function bumpStableVersion(version, bumpType) {
const parts = parseStableVersion(version);
if (!parts) throw new Error(`Invalid stable version: ${version}`);
const [major, minor, patch] = parts;
if (bumpType === "major") return `${major + 1}.0.0`;
if (bumpType === "minor") return `${major}.${minor + 1}.0`;
if (bumpType === "patch") return `${major}.${minor}.${patch + 1}`;
throw new Error(`Unknown bump type: ${bumpType}`);
}
export function readStableTagVersions(cwd = root) {
return execFileSync("git", ["tag", "--list", "v*"], { cwd, encoding: "utf8" })
.split("\n")
.map((tag) => stableVersionFromTag(tag))
.filter((version) => version !== null);
}
function main() {
const args = process.argv.slice(2);
const command = args[0];
const latest = highestStableVersion(readStableTagVersions());
if (command === "latest") {
if (!latest) throw new Error("No stable v* tags found (need a full clone: fetch-depth 0).");
console.log(latest);
return;
}
if (command === "next") {
if (!latest) throw new Error("No stable v* tags found (need a full clone: fetch-depth 0).");
const bumpIndex = args.indexOf("--bump");
const bumpType = bumpIndex >= 0 ? args[bumpIndex + 1] : "patch";
console.log(bumpStableVersion(latest, bumpType));
return;
}
throw new Error("Usage: versions.mjs latest | next --bump patch|minor|major");
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}