#3333 · milestone v10.0.0

sort -V is not semver

Eight 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.

sites fixed 8 plan named 11, two do not exist verified on 2 sort impls tests 23 pass

One line, and it reads 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.

Comparator: try to find a pair where the fix is wrong

Both implementations run live below. The fix translates semver's - to Debian's ~, which sort -V already ranks before the empty string, then sorts.

vs
sort -V says higher
 
semver_max says higher
 

The table both sorts were checked against

LowerHighersort -Vsemver_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.

The eight sites

FileWasConsequence if left
release.yml:171sort -V | tail -1 Docker :latest moves to the alpha image
version-check.yml:123sort -V | tail -1 a PR moving the version backwards passes the required check
pre-push:78sort -V | tail -1 same, locally, before it ever reaches CI
cc-stale-belowfloor.sh:46sort -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:70awk $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.

Why release.yml:171 is the sharp one

Most 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.

The fix, and why it is not a version parser

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.