fix(update): never treat an older registry version as an update (downgrade on 3.60.0) (#4432)

getUpdateType compared major, minor and patch independently. With 3.60.0
running and 3.59.17 as the registry latest, the minor check failed but
patch(17) > patch(0) returned 'patch', so the startup check auto-installed
3.59.17: a downgrade. It also made 'ruflo init' run npm install in the project
and time out the mods-cli-bin test on the 3.60.0 release PR.

An update is a strictly newer version: guard with semver.gt first.

Co-authored-by: r <r@x>
This commit is contained in:
rUvandr authored and GitHub committed 2026-10-23 12:48:58 -04:00
1 parent e8c98eb916
commit 4efb421e33
2 files changed
+35 -2

No files matched your search

@@ -0,0 +1,31 @@
/**
* getUpdateType compared major, minor and patch independently, so a CLI AHEAD of the registry's `latest` in minor but behind it in
* patch (3.60.0 running, latest 3.59.17) read as a 'patch' update (17 > 0) and the startup check auto-installed the older version:
* a downgrade. An update is a strictly newer version, and only that.
*/
import { describe, expect, it } from 'vitest';
import { getUpdateType } from '../src/update/checker.js';
describe('getUpdateType', () => {
it('never reports a version that is not newer as an update (the 3.60.0 vs 3.59.17 downgrade)', () => {
expect(getUpdateType('3.60.0', '3.59.17')).toBe('none');
expect(getUpdateType('3.58.0', '3.57.2')).toBe('none');
expect(getUpdateType('4.0.0', '3.99.99')).toBe('none');
expect(getUpdateType('3.59.17', '3.59.16')).toBe('none');
expect(getUpdateType('3.59.17', '3.59.17')).toBe('none');
expect(getUpdateType('3.7.0-alpha.2', '3.7.0-alpha.1')).toBe('none');
});
it('still names a newer version by how far ahead it is', () => {
expect(getUpdateType('3.59.16', '3.59.17')).toBe('patch');
expect(getUpdateType('3.59.17', '3.60.0')).toBe('minor');
expect(getUpdateType('3.59.17', '3.60.3')).toBe('minor');
expect(getUpdateType('3.59.17', '4.0.0')).toBe('major');
expect(getUpdateType('3.9.9', '3.10.0')).toBe('minor');
});
it('reads an invalid version as no update', () => {
expect(getUpdateType('latest', '3.59.17')).toBe('none');
expect(getUpdateType('3.59.17', 'next')).toBe('none');
});
});
+4 -2
View File
@@ -81,7 +81,7 @@ async function fetchPackageInfo(packageName: string): Promise<NpmPackageInfo | n
}
}
function getUpdateType(
export function getUpdateType(
current: string,
latest: string
): 'major' | 'minor' | 'patch' | 'none' {
@@ -89,7 +89,9 @@ function getUpdateType(
return 'none';
}
if (semver.eq(current, latest)) {
// An update is a strictly newer version. Comparing the parts one by one called 3.60.0 (running) against 3.59.17 (latest) a
// 'patch' update, since 17 > 0, and the startup check then auto-installed the older version: a downgrade.
if (!semver.gt(latest, current)) {
return 'none';
}