sort -V is not semverEight places asked the shell which of two versions is newer. For a plain release they were right. For a prerelease every one of them was backwards.
Semver 2.0.0 is explicit: a prerelease has lower precedence than the release
it precedes. sort -V reads the suffix as extra version content, so it
ranks it higher.
printf '10.0.0\n10.0.0-alpha.1\n' | sort -V | tail -1 gives 10.0.0-alpha.1 wrong semver 10.0.0 correct
Not a theoretical edge. During the v10 alpha train every one of these comparisons runs against a prerelease, on every push and every release.
Both implementations run live below. The fix translates semver's - to
Debian's ~, which sort -V already ranks before the empty
string, then sorts.
| Lower | Higher | sort -V | semver_max |
|---|
Identical results on sort (GNU coreutils) 9.7 (ubuntu-latest,
CI) and sort 2.3-Apple (199) (macOS, local hooks), checked in a container
rather than assumed. tests/unit/test-semver-compare.sh pins the table on
whichever platform runs it, so a future coreutils change that breaks ~
ordering fails the suite instead of silently misranking a release.
| File | Was | Consequence if left |
|---|---|---|
| release.yml:171 | sort -V | tail -1 | Docker :latest moves to the alpha image |
| version-check.yml:123 | sort -V | tail -1 | a PR moving the version backwards passes the required check |
| pre-push:78 | sort -V | tail -1 | same, locally, before it ever reaches CI |
| cc-stale-belowfloor.sh:46 | sort -V | head -1 | a CC build below the floor judged current |
| version-check.yml:158 | "$PR_VERSION" in a regex | a heading for 10x0x0 satisfies a lookup for 10.0.0 |
| release.yml:70 | awk $0 ~ ver |
same, when cutting release notes |
| release-announce.yml:84 | ${VERSION##*.} | reads the alpha ordinal as the patch number |
| stamp-whats-new.mjs:51 | \d+\.\d+\.\d+ | alpha silently absent from README's What's New |
The last two were not in the plan's list of eleven. Two that were,
scripts/bump-version.sh and scripts/create-release.sh,
do not exist.
release.yml:171 is the sharp oneMost of these fail a gate, which is loud. That one moves a Docker tag, which is silent and reaches users.
HIGHEST=$(git tag -l 'v*' | sed 's/^v//' | sort -V | tail -1)
if [ "$VERSION" = "$HIGHEST" ]; then TAGS+=(-t "$IMAGE:latest"); fi
tags present v9.8.0, v10.0.0-alpha.1
sort -V picks 10.0.0-alpha.1 -> :latest points at the alpha
semver picks 10.0.0-alpha.1 is lower than any GA, so :latest stays put
The same shape of problem exists one level up and is not fixed here:
marketplace.json uses source: ./plugins/ork, resolved at
main, so merging an alpha version there moves every plugin user onto it
regardless of the GitHub "Pre-release" badge. That needs its own answer before an
alpha is cut.
10.0.0-alpha.1 -> 10.0.0~alpha.1 -> sort -V -> map back
Debian's ~ means exactly what semver's - means: sorts before
the empty string. Translating one to the other borrows an ordering
sort -V already implements correctly, so there is no hand-written version
comparison to get wrong, and no new dependency.
Only the FIRST - is the prerelease separator. Later ones
belong to the identifier, as in 1.0.0-alpha-1, and are left alone.