fix(check): fail when the manifest is behind the tag, not merely different
CI / Check / Test (push) Failing after 0s
CI / Tag release (push) Skipped

`task check` asserted plugin.json's version EQUALS the latest tag. `git
describe` is local-only, so any checkout that had not fetched the tag CI just
pushed went red on a commit that was correct — three times in one backlog
session, on the loop push → CI tags and pushes the bump → pull → red until you
also fetch tags.

The fault the assertion exists for is the manifest failing to move: a release
whose manifest still names the old version, so no installed host ever sees the
update. That is manifest < tag. Ahead is normal and is now quiet.

Ordering needed a real comparison, and bin/doctor had already grown one for
#30, so it moves to bin/newer-version with its own tests rather than existing
twice. Seven cases including the lexical trap (0.8.10 beats 0.8.9) and a
missing component counting as zero.

Verified by mutation: manifest 0.1.0 against tag v0.9.0 exits 201 naming both
versions; manifest 9.9.9 exits 0; the real tree exits 0.

Closes #34
Bump-Type: patch
This commit is contained in:
2026-08-29 10:07:27 +02:00
parent 025f01aa80
commit 15737321ac
4 changed files with 78 additions and 11 deletions
+8 -2
View File
@@ -104,8 +104,14 @@ tasks:
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
tag_v=$(git -C '{{.TASKFILE_DIR}}' describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo '')
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ]; then
echo "ERROR: plugin.json version ($manifest_v) != latest tag ($tag_v)" >&2
# Ordering, not equality. The fault this guards is the manifest
# failing to MOVE — a release tag whose manifest still names the old
# version, so no installed host ever sees the update. A manifest
# ahead of the newest tag this checkout knows about is the normal
# state between the tagger's bump commit and a tag fetch (#34).
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ] \
&& [ "$(sh '{{.TASKFILE_DIR}}/bin/newer-version' "$manifest_v" "$tag_v")" = "$tag_v" ]; then
echo "ERROR: plugin.json version ($manifest_v) is behind the latest tag ($tag_v) — installed hosts never see the update" >&2
fail=1
fi
else
+3 -9
View File
@@ -252,16 +252,10 @@ manifest_version() {
}
# newer_version a b — echoes whichever sorts higher on major.minor.patch.
# Lives in bin/newer-version because `task check` needs the same ordering (#34)
# and a second copy is a second thing to get wrong.
newer_version() {
printf '%s\n%s\n' "$1" "$2" | awk -F. '
{ raw[NR] = $0; for (i = 1; i <= 3; i++) n[NR, i] = ($i == "" ? 0 : $i + 0) }
END {
for (i = 1; i <= 3; i++) {
if (n[1, i] > n[2, i]) { print raw[1]; exit }
if (n[1, i] < n[2, i]) { print raw[2]; exit }
}
print raw[1]
}'
sh "$SELF_DIR/newer-version" "$1" "$2"
}
installed_v=''
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
# newer-version A B — echo whichever of two dotted versions sorts higher.
# Equal versions echo A. Missing components count as zero, so 0.9 == 0.9.0.
#
# Exists because two callers need ordering rather than equality:
# - bin/doctor, comparing the installed plugin generation against the newest
# manifest on the host (#30);
# - task check, which must fail when plugin.json is BEHIND the latest tag and
# stay quiet when it is ahead — the normal state on a checkout that has not
# fetched the tag CI just pushed (#34).
#
# sort -V would do it in one line and is not portable enough for this fleet.
set -u
[ $# -eq 2 ] || { echo "usage: newer-version A B" >&2; exit 2; }
printf '%s\n%s\n' "$1" "$2" | awk -F. '
{ raw[NR] = $0; for (i = 1; i <= 3; i++) n[NR, i] = ($i == "" ? 0 : $i + 0) }
END {
for (i = 1; i <= 3; i++) {
if (n[1, i] > n[2, i]) { print raw[1]; exit }
if (n[1, i] < n[2, i]) { print raw[2]; exit }
}
print raw[1]
}'
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
# Tests for bin/newer-version.
#
# Extracted from bin/doctor (#30) once `task check` needed the same comparison
# (#34): check asserted plugin.json == latest tag, which is red on any checkout
# that has not fetched the tag the CI tagger just pushed. The assertion it
# actually wants is "not behind", and ordering needs a real comparison.
set -u
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SUT="$ROOT/bin/newer-version"
pass=0
fail=0
if [ ! -f "$SUT" ]; then
printf 'FAIL: %s does not exist\n' "$SUT"
exit 1
fi
# expect <winner> <a> <b> <label>
expect() {
_got=$(sh "$SUT" "$2" "$3")
if [ "$_got" = "$1" ]; then
pass=$((pass + 1)); printf ' ok %s\n' "$4"
else
fail=$((fail + 1)); printf ' FAIL %s (newer-version %s %s -> %s, want %s)\n' "$4" "$2" "$3" "$_got" "$1"
fi
}
expect 0.9.0 0.9.0 0.8.5 'higher minor wins, left'
expect 0.9.0 0.8.5 0.9.0 'higher minor wins, right'
expect 0.8.10 0.8.10 0.8.9 'numeric compare, not lexical — 10 beats 9'
expect 1.0.0 1.0.0 0.99.99 'major dominates'
expect 0.8.5 0.8.5 0.8.5 'equal returns the value'
expect 0.9.1 0.9.1 0.9 'a missing component counts as zero'
expect 0.9 0.9 0.9.0 'equal with a missing component returns the left, per contract'
printf '\nnewer_version: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]