From f8e29ded94f545469e4008a563abae6ba4864dcf Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 27 Apr 2026 22:48:57 +0300 Subject: [PATCH] fix: ignore stale downgrade update suggestions Prevents older reported versions from showing as updates Falls back to registry data when update API is stale --- packages/web/server/lib/package-manager.js | 31 +++++++++++++--------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/web/server/lib/package-manager.js b/packages/web/server/lib/package-manager.js index f6eaff38..1dec6959 100644 --- a/packages/web/server/lib/package-manager.js +++ b/packages/web/server/lib/package-manager.js @@ -117,8 +117,11 @@ async function checkForUpdatesFromApi(currentVersion, options = {}) { const data = await response.json(); if (typeof data?.latestVersion !== 'string') return null; + const versionComparison = compareVersions(data.latestVersion, currentVersion); + if (versionComparison < 0) return null; + return { - available: Boolean(data.updateAvailable), + available: Boolean(data.updateAvailable) && versionComparison > 0, version: data.latestVersion, currentVersion, body: typeof data.releaseNotes === 'string' ? data.releaseNotes : undefined, @@ -650,11 +653,19 @@ export async function getLatestVersion() { } /** - * Parse semver version to numeric for comparison + * Compare semver-like version strings. */ -function parseVersion(version) { - const parts = version.replace(/^v/, '').split('.').map(Number); - return (parts[0] || 0) * 10000 + (parts[1] || 0) * 100 + (parts[2] || 0); +function compareVersions(left, right) { + const a = String(left || '').replace(/^v/, '').split('.').map((part) => Number.parseInt(part || '0', 10)); + const b = String(right || '').replace(/^v/, '').split('.').map((part) => Number.parseInt(part || '0', 10)); + const length = Math.max(a.length, b.length); + + for (let index = 0; index < length; index += 1) { + const diff = (a[index] || 0) - (b[index] || 0); + if (diff !== 0) return diff; + } + + return 0; } /** @@ -671,14 +682,10 @@ export async function fetchChangelogNotes(fromVersion, toVersion) { const changelog = await response.text(); const sections = changelog.split(/^## /m).slice(1); - const fromNum = parseVersion(fromVersion); - const toNum = parseVersion(toVersion); - const relevantSections = sections.filter((section) => { const match = section.match(/^\[(\d+\.\d+\.\d+)\]/); if (!match) return false; - const ver = parseVersion(match[1]); - return ver > fromNum && ver <= toNum; + return compareVersions(match[1], fromVersion) > 0 && compareVersions(match[1], toVersion) <= 0; }); if (relevantSections.length === 0) return undefined; @@ -716,9 +723,7 @@ export async function checkForUpdates(options = {}) { }; } - const currentNum = parseVersion(currentVersion); - const latestNum = parseVersion(latestVersion); - const available = latestNum > currentNum; + const available = compareVersions(latestVersion, currentVersion) > 0; let changelog; if (available) { changelog = await fetchChangelogNotes(currentVersion, latestVersion);