fix(update): compare core versions using SemVer
Only show update indicators when the remote core version is newer. Add coverage for prereleases, build metadata, invalid versions, and version boundaries.
This commit is contained in:
1 parent
355d8057e6
commit
45ecd37ce5
3 files changed
+105
-4
No files matched your search
+56
-3
@@ -35,13 +35,66 @@ function normalizeVersion(version: string | null | undefined) {
|
||||
return (version ?? '').trim().replace(/^v/i, '');
|
||||
}
|
||||
|
||||
type SemanticVersion = {
|
||||
core: [string, string, string];
|
||||
prerelease: string[] | null;
|
||||
};
|
||||
|
||||
const semanticVersionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||
|
||||
function parseSemanticVersion(version: string | null | undefined): SemanticVersion | null {
|
||||
const match = semanticVersionPattern.exec(normalizeVersion(version));
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
core: [match[1], match[2], match[3]],
|
||||
prerelease: match[4] ? match[4].split('.') : null,
|
||||
};
|
||||
}
|
||||
|
||||
function compareNumericIdentifier(left: string, right: string) {
|
||||
if (left.length !== right.length) return left.length > right.length ? 1 : -1;
|
||||
if (left === right) return 0;
|
||||
return left > right ? 1 : -1;
|
||||
}
|
||||
|
||||
function compareSemanticVersions(left: SemanticVersion, right: SemanticVersion) {
|
||||
for (let index = 0; index < left.core.length; index += 1) {
|
||||
const difference = compareNumericIdentifier(left.core[index], right.core[index]);
|
||||
if (difference !== 0) return difference;
|
||||
}
|
||||
|
||||
if (left.prerelease === null) return right.prerelease === null ? 0 : 1;
|
||||
if (right.prerelease === null) return -1;
|
||||
|
||||
const count = Math.max(left.prerelease.length, right.prerelease.length);
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const leftIdentifier = left.prerelease[index];
|
||||
const rightIdentifier = right.prerelease[index];
|
||||
if (leftIdentifier === undefined) return -1;
|
||||
if (rightIdentifier === undefined) return 1;
|
||||
if (leftIdentifier === rightIdentifier) continue;
|
||||
|
||||
const leftIsNumeric = /^\d+$/.test(leftIdentifier);
|
||||
const rightIsNumeric = /^\d+$/.test(rightIdentifier);
|
||||
if (leftIsNumeric && rightIsNumeric) {
|
||||
return compareNumericIdentifier(leftIdentifier, rightIdentifier);
|
||||
}
|
||||
if (leftIsNumeric) return -1;
|
||||
if (rightIsNumeric) return 1;
|
||||
return leftIdentifier > rightIdentifier ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function coreUpdateAvailable(
|
||||
currentVersion: string | null | undefined,
|
||||
latestVersion: string | null | undefined,
|
||||
) {
|
||||
const current = normalizeVersion(currentVersion);
|
||||
const latest = normalizeVersion(latestVersion);
|
||||
return Boolean(current && latest && current !== latest);
|
||||
const current = parseSemanticVersion(currentVersion);
|
||||
const latest = parseSemanticVersion(latestVersion);
|
||||
return Boolean(current && latest && compareSemanticVersions(latest, current) > 0);
|
||||
}
|
||||
|
||||
export function requestLatestCore(force = false) {
|
||||
|
||||
@@ -742,7 +742,7 @@ export function VersionManagementPage() {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={latestVersion && (!coreInstalled || currentVersion !== latestVersion) ? 'primary-button' : 'secondary-button'}
|
||||
className={latestVersion && (!coreInstalled || coreHasUpdate) ? 'primary-button' : 'secondary-button'}
|
||||
title={latestVersion ? t('kernel.versions.stopAndUpdateVersion', { version: latestVersion }) : t('kernel.versions.installLatest')}
|
||||
disabled={!latestVersion || busy}
|
||||
onClick={() => setConfirmUpdateOpen(true)}
|
||||
|
||||
@@ -28,7 +28,55 @@ describe('VersionManagement helper functions', () => {
|
||||
it('compares installed and latest core versions consistently', () => {
|
||||
expect(coreUpdateAvailable('v6.6.0', '6.6.0')).toBe(false);
|
||||
expect(coreUpdateAvailable('6.5.0', 'v6.6.0')).toBe(true);
|
||||
expect(coreUpdateAvailable('v7.2.151', 'v7.2.150')).toBe(false);
|
||||
expect(coreUpdateAvailable('6.9.0', '6.10.0')).toBe(true);
|
||||
expect(coreUpdateAvailable('6.10.0', '6.9.0')).toBe(false);
|
||||
expect(coreUpdateAvailable('6.6.0-rc.2', '6.6.0-rc.10')).toBe(true);
|
||||
expect(coreUpdateAvailable('6.6.0-rc.1', '6.6.0')).toBe(true);
|
||||
expect(coreUpdateAvailable('6.6.0', '6.6.0-rc.1')).toBe(false);
|
||||
expect(coreUpdateAvailable('6.6.0+local', '6.6.0+remote')).toBe(false);
|
||||
expect(coreUpdateAvailable(null, '6.6.0')).toBe(false);
|
||||
expect(coreUpdateAvailable('6.5.0', '')).toBe(false);
|
||||
expect(coreUpdateAvailable('dev', '6.6.0')).toBe(false);
|
||||
expect(coreUpdateAvailable('6.5.0', 'unknown')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show an update indicator for an older core release', () => {
|
||||
const coreHasUpdate = coreUpdateAvailable('v7.2.151', 'v7.2.150');
|
||||
expect(appUpdateIndicatorState(false, coreHasUpdate, false)).toBeNull();
|
||||
});
|
||||
|
||||
it('follows the semantic version precedence rules', () => {
|
||||
const orderedVersions = [
|
||||
'1.0.0-alpha',
|
||||
'1.0.0-alpha.1',
|
||||
'1.0.0-alpha.beta',
|
||||
'1.0.0-beta',
|
||||
'1.0.0-beta.2',
|
||||
'1.0.0-beta.11',
|
||||
'1.0.0-rc.1',
|
||||
'1.0.0',
|
||||
'1.0.1',
|
||||
'1.1.0',
|
||||
'2.0.0',
|
||||
];
|
||||
|
||||
for (let index = 1; index < orderedVersions.length; index += 1) {
|
||||
const older = orderedVersions[index - 1];
|
||||
const newer = orderedVersions[index];
|
||||
expect(coreUpdateAvailable(older, newer)).toBe(true);
|
||||
expect(coreUpdateAvailable(newer, older)).toBe(false);
|
||||
expect(coreUpdateAvailable(newer, newer)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles prefixes, whitespace, large identifiers, and invalid versions', () => {
|
||||
expect(coreUpdateAvailable(' V7.2.150 ', ' v7.2.151 ')).toBe(true);
|
||||
expect(coreUpdateAvailable('999999999999999999.0.0', '1000000000000000000.0.0')).toBe(true);
|
||||
expect(coreUpdateAvailable('1.0.0-999999999999999999', '1.0.0-1000000000000000000')).toBe(true);
|
||||
expect(coreUpdateAvailable('1.0.0+build.1', '1.0.0+build.2')).toBe(false);
|
||||
expect(coreUpdateAvailable('1.0.0', '1.01.0')).toBe(false);
|
||||
expect(coreUpdateAvailable('1.0', '1.0.1')).toBe(false);
|
||||
expect(coreUpdateAvailable('1.0.0.0', '1.0.1')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user