Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/upgrade/github-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,19 @@ export async function fetchLatestRelease(repo: string = RELEASE_REPO): Promise<R
const all = (await res.json()) as Release[];
const cliReleases = all.filter((r) => !r.draft && r.tag_name.startsWith("cli-v"));
if (cliReleases.length === 0) return null;
// Sort by version (highest first). GitHub returns releases by created_at,
// so a hot-fix on an older minor that's published *after* a newer release
// would otherwise appear at index 0 and trick `upgrade` into doing nothing
// (or downgrading).
cliReleases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
return cliReleases[0]!;
}

export function versionFromTag(tag: string): string {
return tag.replace(/^cli-v/, "");
}

/** Returns negative if a is older than b. Small-but-deterministic semver. */
/** Returns negative if a is older than b. SemVer 2.0.0 precedence rules. */
export function compareVersions(a: string, b: string): number {
const aRel = versionFromTag(a);
const bRel = versionFromTag(b);
Expand All @@ -64,11 +69,48 @@ export function compareVersions(a: string, b: string): number {
if (aPre && !bPre) return -1;
if (!aPre && bPre) return 1;
if (!aPre && !bPre) return 0;
return aPre!.localeCompare(bPre!);
return comparePrerelease(aPre!, bPre!);
}

function splitVersion(v: string): [number[], string | null] {
const [core, pre] = v.split("-", 2);
const parts = (core ?? "0.0.0").split(".").map((n) => Number(n)).map((n) => (Number.isFinite(n) ? n : 0));
return [parts, pre ?? null];
}

/**
* Compare two SemVer prerelease strings (the part after the `-`) per the
* SemVer 2.0.0 precedence rules:
* - Identifiers consisting of only digits are compared numerically.
* - Identifiers with letters or hyphens are compared lexically in ASCII.
* - Numeric identifiers always have lower precedence than non-numeric.
* - A larger set of fields has higher precedence than a smaller one.
*
* Crucially, this means `alpha.10` > `alpha.2` (numeric compare on the
* second identifier), which a naive `String#localeCompare` gets wrong.
*/
function comparePrerelease(a: string, b: string): number {
const aIds = a.split(".");
const bIds = b.split(".");
const max = Math.max(aIds.length, bIds.length);
for (let i = 0; i < max; i++) {
const ai = aIds[i];
const bi = bIds[i];
if (ai === undefined) return -1;
if (bi === undefined) return 1;
const aNum = /^\d+$/.test(ai);
const bNum = /^\d+$/.test(bi);
if (aNum && bNum) {
const d = Number(ai) - Number(bi);
if (d !== 0) return d;
} else if (aNum) {
return -1;
} else if (bNum) {
return 1;
} else {
const d = ai.localeCompare(bi);
if (d !== 0) return d;
}
}
return 0;
}