Skip to content

Commit b5bf24f

Browse files
authored
fix(release): fail closed on unparseable channel versions (#1753 follow-up) (#1757)
compareReleaseVersions produced NaN for build-metadata cores (2.19.3+build.1), letting the channel forward-guard pass any candidate. Parse strictly per SemVer 2.0.0 (build metadata ignored for precedence) and throw on unparseable input; the caller exits 1.
1 parent c55840b commit b5bf24f

2 files changed

Lines changed: 36 additions & 4 deletions

File tree

‎scripts/release.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,15 @@ async function githubReleaseExists(tagName: string): Promise<boolean> {
157157
/** Order two semver strings per the semver.org rules (numeric identifiers numerically,
158158
* numeric < alphanumeric prerelease, prerelease < release). Returns negative/0/positive. */
159159
export function compareReleaseVersions(left: string, right: string): number {
160+
// SemVer 2.0.0: build metadata (+...) is valid and ignored for precedence, but
161+
// anything else unparseable must fail CLOSED. Number() on a garbage core used to
162+
// yield NaN, and NaN comparisons made the forward guard pass any candidate.
163+
const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
160164
const parse = (value: string) => {
161-
const [main, ...preParts] = value.split("-");
162-
const nums = (main ?? "").split(".").map(part => Number(part));
163-
return { nums, pre: preParts.length ? preParts.join("-").split(".") : null };
165+
const match = SEMVER.exec(value.trim());
166+
if (!match) throw new Error(`unparseable release version: ${JSON.stringify(value)}`);
167+
const nums = [Number(match[1]), Number(match[2]), Number(match[3])];
168+
return { nums, pre: match[4] ? match[4].split(".") : null };
164169
};
165170
const a = parse(left);
166171
const b = parse(right);
@@ -206,7 +211,14 @@ async function assertChannelVersionMovesForward(packageName: string, version: st
206211
}
207212
const current = distTags[channel];
208213
if (!current) return; // channel not published yet — nothing to regress
209-
if (compareReleaseVersions(version, current) <= 0) {
214+
let forward: number;
215+
try {
216+
forward = compareReleaseVersions(version, current);
217+
} catch (err) {
218+
console.error(`✗ cannot compare release versions (candidate ${version}, channel tip ${JSON.stringify(current)}): ${err instanceof Error ? err.message : String(err)}`);
219+
process.exit(1);
220+
}
221+
if (forward <= 0) {
210222
console.error(`✗ release version ${version} does not move the '${channel}' channel forward (current: ${current}).`);
211223
console.error("Reconcile the version line first: dev's package.json may trail the latest release; pick a version strictly newer than the channel tip.");
212224
process.exit(1);

‎tests/release-helper.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,4 +396,24 @@ describe("release helper", () => {
396396
// And the launcher must still be the thing it reaches for.
397397
expect(withoutComments).toContain("commandInvocation");
398398
});
399+
400+
// #1753 review follow-up: build metadata on the channel tip is valid semver
401+
// and compares by precedence only; an unparseable tip must fail CLOSED
402+
// (Number() on a garbage core used to yield NaN and pass any candidate).
403+
test("channel tip with build metadata compares by precedence, not NaN", () => {
404+
const { result } = runRelease("2.19.4", { npmLatest: "2.19.3+build.1" });
405+
expect(`${result.status}\n${result.stderr ?? ""}`.trim()).toBe("0");
406+
});
407+
408+
test("channel tip equal after stripping build metadata does not move forward", () => {
409+
const { result } = runRelease("2.19.3", { npmLatest: "2.19.3+build.1" });
410+
expect(result.status).toBe(1);
411+
expect(result.stderr ?? "").toContain("does not move");
412+
});
413+
414+
test("unparseable channel tip fails closed", () => {
415+
const { result } = runRelease("2.19.4", { npmLatest: "not-a-version" });
416+
expect(result.status).toBe(1);
417+
expect(result.stderr ?? "").toContain("cannot compare release versions");
418+
});
399419
});

0 commit comments

Comments
 (0)