mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
A plain string compare on the prerelease tag put "alpha11" below "alpha8", because
'1' < '8'. The OTA check therefore answered client-newer and refused to offer the
update — while reporting the newer build as `latest` in the same response:
latest_version: 1.9.34-alpha11 current_version: 1.9.34-alpha8
update_available: false reason: "client-newer"
So a fleet on alpha8 or alpha9 could not be moved forward at all, silently, and
nothing about the symptom pointed at version ordering. alpha10 was never really on
offer either; the last update that genuinely worked was alpha6 -> alpha8, where the
lexical order happens to be right by luck.
This is what semver specifies for a single alphanumeric identifier, and it is
simply not what the naming means. lib/version-precedence.js compares digit runs
NUMERICALLY, so alpha8 < alpha9 < alpha10 < alpha11, while leaving everything else
alphabetical — beta still outranks alpha, rc still outranks beta, and a release
still outranks any prerelease of the same core.
Dot-separated identifiers are compared per semver and a shorter run loses, so
moving the naming to the semver-correct `-alpha.11` form later needs no further
change here.
TWO comparators carried the assumption, each with a comment asserting lexical was
fine "for our naming" — true only while the counter stayed below 10. Both now use
the shared helper rather than a third copy drifting into the same trap:
- lib/ota-breaker.js the Android OTA path
- lib/brightsign-update.js the BrightSign host package, where a wrong-way
comparison replaces the script that boots the player
lib/ghcr-check.js was checked and is unaffected: it rejects prerelease strings
outright rather than ordering them.
Tests pin the exact stranding case end to end — decide('1.9.34-alpha8',
'1.9.34-alpha11') must be an offer, not client-newer — plus the reverse direction,
so a future change cannot merely invert it.
1668/1668 pass.
63 lines
2.9 KiB
JavaScript
63 lines
2.9 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
* Precedence for PRERELEASE identifiers — the `-alpha11` half of `1.9.34-alpha11`.
|
|
*
|
|
* ⚠️ WHY THIS EXISTS: a plain string compare is what semver specifies for a single alphanumeric
|
|
* identifier, and it is wrong for how this project actually names builds. `"alpha11" < "alpha8"`
|
|
* because `'1' < '8'`, so EVERY build from alpha10 onward sorted below alpha8 and alpha9. The OTA
|
|
* check then answered `client-newer` and refused to offer the update at all — a fleet on alpha8
|
|
* could not be moved forward, silently, with the server reporting the newer build as `latest` in
|
|
* the same breath. Two comparators carried the same assumption, both with a comment saying lexical
|
|
* was "fine for our naming"; it was fine only while the counter stayed below 10.
|
|
*
|
|
* The rule here is natural ordering: split each identifier into digit and non-digit runs and
|
|
* compare digit runs NUMERICALLY. That gives what a human means by the name — alpha8 < alpha9 <
|
|
* alpha10 < alpha11 — while leaving everything else alphabetical, so beta still outranks alpha and
|
|
* rc still outranks beta.
|
|
*
|
|
* Dot-separated identifiers are compared one at a time per semver, and a shorter run of identifiers
|
|
* loses when all preceding ones are equal (`alpha` < `alpha.1`), so a future move to the semver-
|
|
* correct `-alpha.11` form keeps working without another change here.
|
|
*
|
|
* Deliberately NOT handled: whether a prerelease outranks a release. That is the caller's rule —
|
|
* both callers already implement it, and each has its own exceptions (ota-breaker treats the legacy
|
|
* `-patchN` scheme as released).
|
|
*/
|
|
|
|
// Compare one identifier, digit runs numerically. "alpha10" -> ["alpha", "10"].
|
|
function naturalCmp(x, y) {
|
|
const rx = String(x).match(/\d+|\D+/g) || [];
|
|
const ry = String(y).match(/\d+|\D+/g) || [];
|
|
for (let i = 0; i < Math.max(rx.length, ry.length); i++) {
|
|
const a = rx[i], b = ry[i];
|
|
if (a === undefined) return -1; // "alpha" < "alpha1"
|
|
if (b === undefined) return 1;
|
|
const aNum = /^\d+$/.test(a), bNum = /^\d+$/.test(b);
|
|
if (aNum && bNum) {
|
|
// Numeric, so 10 beats 8 — the whole point of this file.
|
|
if (Number(a) !== Number(b)) return Number(a) < Number(b) ? -1 : 1;
|
|
} else if (a !== b) {
|
|
// A digit run sorts below a word run, matching semver's numeric-identifiers-first rule.
|
|
if (aNum !== bNum) return aNum ? -1 : 1;
|
|
return a < b ? -1 : 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Full prerelease precedence: dot-separated identifiers, each compared naturally. */
|
|
function preCmp(a, b) {
|
|
if (a === b) return 0;
|
|
const as = String(a).split('.'), bs = String(b).split('.');
|
|
for (let i = 0; i < Math.max(as.length, bs.length); i++) {
|
|
if (as[i] === undefined) return -1; // "alpha" < "alpha.1"
|
|
if (bs[i] === undefined) return 1;
|
|
const c = naturalCmp(as[i], bs[i]);
|
|
if (c !== 0) return c;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
module.exports = { preCmp, naturalCmp };
|