diff --git a/scripts/release.test.ts b/scripts/release.test.ts index cc820cb76..c7f640715 100644 --- a/scripts/release.test.ts +++ b/scripts/release.test.ts @@ -1,36 +1,38 @@ import { describe, expect, test } from "bun:test"; -import { isValidExplicitVersion } from "./release"; +import { validateExplicitVersion } from "./release"; -describe("isValidExplicitVersion", () => { +describe("validateExplicitVersion", () => { test("rejects malformed versions", () => { - expect(isValidExplicitVersion("999.bad")).toBe(false); - expect(isValidExplicitVersion("17")).toBe(false); - expect(isValidExplicitVersion("17.2")).toBe(false); - expect(isValidExplicitVersion("17.2.8.9")).toBe(false); - expect(isValidExplicitVersion("v17.2.8.9")).toBe(false); - expect(isValidExplicitVersion("abc")).toBe(false); - expect(isValidExplicitVersion("")).toBe(false); - expect(isValidExplicitVersion("v")).toBe(false); - expect(isValidExplicitVersion("17.2.8-")).toBe(false); + expect(validateExplicitVersion("999.bad")).toBe(null); + expect(validateExplicitVersion("17")).toBe(null); + expect(validateExplicitVersion("17.2")).toBe(null); + expect(validateExplicitVersion("17.2.8.9")).toBe(null); + expect(validateExplicitVersion("v17.2.8.9")).toBe(null); + expect(validateExplicitVersion("abc")).toBe(null); + expect(validateExplicitVersion("")).toBe(null); + expect(validateExplicitVersion("v")).toBe(null); + expect(validateExplicitVersion("17.2.8-")).toBe(null); }); - test("accepts valid three-segment numeric versions", () => { - expect(isValidExplicitVersion("17.2.8")).toBe(true); - expect(isValidExplicitVersion("0.0.0")).toBe(true); - expect(isValidExplicitVersion("1.0.0")).toBe(true); + test("rejects prerelease suffixes (not supported by this release path)", () => { + // Prereleases would be published as npm `latest` because the downstream + // publish runs `npm publish` with no `--tag`. + expect(validateExplicitVersion("17.2.8-rc.1")).toBe(null); + expect(validateExplicitVersion("v17.2.8-beta")).toBe(null); + expect(validateExplicitVersion("1.0.0-alpha")).toBe(null); + expect(validateExplicitVersion("1.0.0-alpha.1.2")).toBe(null); + expect(validateExplicitVersion("1.0.0-0.3.7")).toBe(null); + expect(validateExplicitVersion("1.0.0-x.7.z.92")).toBe(null); }); - test("accepts leading v prefix", () => { - expect(isValidExplicitVersion("v17.2.8")).toBe(true); - expect(isValidExplicitVersion("V17.2.8")).toBe(false); + test("accepts bare three-segment numeric versions and returns them unchanged", () => { + expect(validateExplicitVersion("17.2.8")).toBe("17.2.8"); + expect(validateExplicitVersion("0.0.0")).toBe("0.0.0"); + expect(validateExplicitVersion("1.0.0")).toBe("1.0.0"); }); - test("accepts prerelease suffixes", () => { - expect(isValidExplicitVersion("17.2.8-rc.1")).toBe(true); - expect(isValidExplicitVersion("v17.2.8-rc.1")).toBe(true); - expect(isValidExplicitVersion("1.0.0-beta")).toBe(true); - expect(isValidExplicitVersion("1.0.0-alpha.1.2")).toBe(true); - expect(isValidExplicitVersion("1.0.0-0.3.7")).toBe(true); - expect(isValidExplicitVersion("1.0.0-x.7.z.92")).toBe(true); + test("accepts leading v prefix and normalizes to the bare version", () => { + expect(validateExplicitVersion("v17.2.8")).toBe("17.2.8"); + expect(validateExplicitVersion("V17.2.8")).toBe(null); }); }); diff --git a/scripts/release.ts b/scripts/release.ts index 4e79926b8..832c3ab5c 100755 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -16,12 +16,22 @@ const changelogGlob = new Glob("packages/*/CHANGELOG.md"); const packageJsonGlob = new Glob("packages/*/package.json"); const cargoTomlGlob = new Glob("crates/*/Cargo.toml"); /** - * Strict explicit-version guard: three numeric dot-segments, optional leading - * `v`, optional SemVer-2.0 prerelease suffix. Bump keywords (major/minor/patch) - * are handled separately and must not be routed through this check. + * Strict explicit-version guard: three numeric dot-segments with an optional + * leading `v` and NO prerelease suffix. Prereleases are rejected because the + * downstream publish (`scripts/ci-release-publish.ts`) runs `npm publish` with + * no `--tag`, which would promote a prerelease to the npm `latest` dist-tag — + * hitting every unqualified install and the `/latest` endpoint `omp update` + * reads. Bump keywords (major/minor/patch) are handled separately and must not + * be routed through this check. + * + * Returns the normalized bare version (leading `v` stripped) when accepted, or + * `null` when rejected. Callers must use the returned value for all writes so + * no downstream manifest (package.json, Cargo.toml, tag) ever sees a `v` + * prefix — Cargo rejects `version = "v17.2.8"`. */ -export function isValidExplicitVersion(version: string): boolean { - return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version); +export function validateExplicitVersion(version: string): string | null { + const match = /^v?(\d+\.\d+\.\d+)$/.exec(version); + return match ? match[1] : null; } function git(args: readonly string[]) { @@ -199,14 +209,18 @@ async function cmdRelease(versionOrBump: string): Promise { console.log("\n=== Release Script ===\n"); // Validate explicit versions before any compare: the shared compareVersions // never throws, so without this guard garbage like "999.bad" would be - // accepted and written into every package.json / Cargo.toml / tag. + // accepted and written into every package.json / Cargo.toml / tag. The + // validator also normalizes a leading `v` to the bare version so every + // downstream write (manifests, Cargo.toml, tag) uses `17.2.8`, not `v17.2.8`. if (versionOrBump !== "major" && versionOrBump !== "minor" && versionOrBump !== "patch") { - if (!isValidExplicitVersion(versionOrBump)) { + const normalized = validateExplicitVersion(versionOrBump); + if (normalized === null) { console.error( - `Error: Invalid version "${versionOrBump}". Expected a semver like 17.2.8 or v17.2.8-rc.1, or a bump keyword (major/minor/patch).`, + `Error: Invalid version "${versionOrBump}". Expected a semver like 17.2.8 or v17.2.8 (prereleases such as 17.2.8-rc.1 are not supported by this release path), or a bump keyword (major/minor/patch).`, ); process.exit(1); } + versionOrBump = normalized; } // 1. Pre-flight checks @@ -421,7 +435,7 @@ if (import.meta.main) { if (arg === "watch") { await cmdWatch(); - } else if (arg === "major" || arg === "minor" || arg === "patch" || isValidExplicitVersion(arg)) { + } else if (arg === "major" || arg === "minor" || arg === "patch" || validateExplicitVersion(arg) !== null) { await cmdRelease(arg); } else { console.error(`Unknown command or invalid version: ${arg}`);