From a4dc13d9c09b017cce22da180def50414e1f43f7 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Thu, 19 Feb 2026 18:33:07 -0500 Subject: [PATCH] fix: handle pre-release versions in compareVersions Closes #356 --- .../update/__tests__/update-action.test.ts | 24 +++++++++ src/cli/commands/update/action.ts | 49 ++++++++++++++++--- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/update/__tests__/update-action.test.ts b/src/cli/commands/update/__tests__/update-action.test.ts index a68705d4..aa317091 100644 --- a/src/cli/commands/update/__tests__/update-action.test.ts +++ b/src/cli/commands/update/__tests__/update-action.test.ts @@ -50,6 +50,30 @@ describe('compareVersions', () => { it('handles versions with missing parts', () => { expect(compareVersions('1.0', '1.0.0')).toBe(0); }); + + it('returns 1 when latest pre-release is newer', () => { + expect(compareVersions('0.3.0-preview.1.0', '0.3.0-preview.2.0')).toBe(1); + }); + + it('returns -1 when current pre-release is newer', () => { + expect(compareVersions('0.3.0-preview.2.0', '0.3.0-preview.1.0')).toBe(-1); + }); + + it('returns 0 for equal pre-release versions', () => { + expect(compareVersions('0.3.0-preview.1.0', '0.3.0-preview.1.0')).toBe(0); + }); + + it('returns 1 when latest is release and current is pre-release', () => { + expect(compareVersions('1.0.0-preview.1', '1.0.0')).toBe(1); + }); + + it('returns -1 when current is release and latest is pre-release', () => { + expect(compareVersions('1.0.0', '1.0.0-preview.1')).toBe(-1); + }); + + it('compares pre-release labels lexicographically', () => { + expect(compareVersions('1.0.0-alpha.1', '1.0.0-beta.1')).toBe(1); + }); }); describe('fetchLatestVersion', () => { diff --git a/src/cli/commands/update/action.ts b/src/cli/commands/update/action.ts index dceadf35..a32f5761 100644 --- a/src/cli/commands/update/action.ts +++ b/src/cli/commands/update/action.ts @@ -14,15 +14,52 @@ export async function fetchLatestVersion(): Promise { } export function compareVersions(current: string, latest: string): number { - const currentParts = current.split('.').map(Number); - const latestParts = latest.split('.').map(Number); + const parse = (v: string) => { + const [core = '', ...prereleaseParts] = v.split('-'); + const nums = core.split('.').map(Number); + const prerelease = prereleaseParts.join('-'); + return { nums, prerelease }; + }; + const curr = parse(current); + const lat = parse(latest); + + // Compare major.minor.patch for (let i = 0; i < 3; i++) { - const curr = currentParts[i] ?? 0; - const lat = latestParts[i] ?? 0; - if (lat > curr) return 1; - if (lat < curr) return -1; + const c = curr.nums[i] ?? 0; + const l = lat.nums[i] ?? 0; + if (l > c) return 1; + if (l < c) return -1; + } + + // Equal core versions — compare pre-release segments + if (!curr.prerelease && !lat.prerelease) return 0; + // A version without pre-release is greater than one with (1.0.0 > 1.0.0-preview) + if (!curr.prerelease) return -1; + if (!lat.prerelease) return 1; + + const currSegments = curr.prerelease.split('.'); + const latSegments = lat.prerelease.split('.'); + const len = Math.max(currSegments.length, latSegments.length); + + for (let i = 0; i < len; i++) { + const cs = currSegments[i]; + const ls = latSegments[i]; + if (cs === undefined) return 1; // fewer segments = earlier + if (ls === undefined) return -1; + const cn = Number(cs); + const ln = Number(ls); + // Both numeric — compare numerically + if (!isNaN(cn) && !isNaN(ln)) { + if (ln > cn) return 1; + if (ln < cn) return -1; + } else { + // Lexicographic comparison for non-numeric segments + if (ls > cs) return 1; + if (ls < cs) return -1; + } } + return 0; }