fix: use semver comparison for extension version upgrades

Previously the system used simple string inequality to detect available
updates, causing downgrades to appear as upgrades (e.g. 2.5.1 → 2.5.0).
Now both backend (SQL) and frontend (JS) compare version tuples properly.
Also fixes latest_version in DB on registry sync startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 15:05:15 +00:00
parent 3cee3abfeb
commit d0b87b16dc
2 changed files with 14 additions and 3 deletions
+12 -2
View File
@@ -156,6 +156,15 @@ def sync_extension_registry():
(name,), fetch=None)
deactivated.append(name)
# Fix latest_version: set to the highest version from extension_versions
query("""UPDATE extensions e SET latest_version = sub.max_ver, updated_at = NOW()
FROM (SELECT extension_id, version as max_ver FROM extension_versions ev1
WHERE NOT EXISTS (SELECT 1 FROM extension_versions ev2
WHERE ev2.extension_id = ev1.extension_id
AND string_to_array(ev2.version, '.')::int[] > string_to_array(ev1.version, '.')::int[])
) sub WHERE e.id = sub.extension_id AND (e.latest_version IS NULL OR e.latest_version != sub.max_ver)""",
fetch=None)
# Reload MODULES from DB
_load_modules_from_db()
@@ -363,8 +372,9 @@ def build_and_publish(module, version):
SELECT id, %s, %s, %s FROM extensions WHERE name = %s
ON CONFLICT (extension_id, version) DO NOTHING""",
(version, f'{module}-{version}.zip', tag, module))
cur.execute("UPDATE extensions SET latest_version = %s, updated_at = NOW() WHERE name = %s",
(version, module))
cur.execute("""UPDATE extensions SET latest_version = %s, updated_at = NOW()
WHERE name = %s AND (latest_version IS NULL OR string_to_array(latest_version, '.')::int[] < string_to_array(%s, '.')::int[])""",
(version, module, version))
conn.commit()
cur.close()
conn.close()
+2 -1
View File
@@ -456,7 +456,8 @@ function CustomerDetail({ customerId, onDelete }) {
<tbody className="divide-y divide-slate-50">
{sorted.map(e => {
const installed = e.installed_version
const hasUpdate = installed && e.latest_version && installed !== e.latest_version
const semverGt = (a, b) => { const pa = a.split('.').map(Number), pb = b.split('.').map(Number); for (let i = 0; i < Math.max(pa.length, pb.length); i++) { if ((pa[i]||0) > (pb[i]||0)) return true; if ((pa[i]||0) < (pb[i]||0)) return false; } return false }
const hasUpdate = installed && e.latest_version && semverGt(e.latest_version, installed)
const missingDeps = (deps[e.name] || []).filter(d => !installedNames.has(d))
const blocked = !installed && missingDeps.length > 0
return (