104 lines
3.5 KiB
JavaScript
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);
|
|
}
|
|
}
|