From 8d3863ba9ddb1b69d1490999492d054cb6e6a295 Mon Sep 17 00:00:00 2001 From: danilaverbena Date: Tue, 21 Jul 2026 01:58:57 +0300 Subject: [PATCH 1/4] fix(arcup): honor SemVer prerelease precedence in version_gt version_gt stripped the prerelease suffix from both operands before comparing, so versions differing only in prerelease (e.g. 0.3.0 vs 0.3.0-rc.1) compared as equal and the installer's self-update check failed to see a stable release as newer than a prerelease. Implement SemVer section 11 precedence: compare core numerically, then apply prerelease rules (no-prerelease outranks prerelease; dot-separated identifiers compared numerically/lexically; more identifiers outrank fewer). Fixes #205. --- arcup/arcup | 51 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/arcup/arcup b/arcup/arcup index 3590cd3..4dff8dc 100755 --- a/arcup/arcup +++ b/arcup/arcup @@ -262,16 +262,24 @@ version_gt() { [ "$1" = "$2" ] && return 1 # Remove 'v' prefix if present - local ver1="${1#v}" - local ver2="${2#v}" - ver1="${ver1%%-*}" - ver2="${ver2%%-*}" - + local v1="${1#v}" + local v2="${2#v}" + + # Split into core "major.minor.patch" and prerelease; drop build metadata (+...), + # which SemVer ignores for precedence. + local core1="${v1%%-*}" pre1="" + [ "$v1" = "$core1" ] || pre1="${v1#*-}" + local core2="${v2%%-*}" pre2="" + [ "$v2" = "$core2" ] || pre2="${v2#*-}" + core1="${core1%%+*}"; core2="${core2%%+*}" + pre1="${pre1%%+*}"; pre2="${pre2%%+*}" + + local major1 minor1 patch1 major2 minor2 patch2 IFS=. read -r major1 minor1 patch1 < lower precedence + [ -z "$id2" ] && return 0 # more identifiers -> higher precedence + [ "$id1" = "$id2" ] && continue + if [[ "$id1" =~ ^[0-9]+$ ]] && [[ "$id2" =~ ^[0-9]+$ ]]; then + [ "$id1" -gt "$id2" ] && return 0 + [ "$id1" -lt "$id2" ] && return 1 + elif [[ "$id1" =~ ^[0-9]+$ ]]; then + return 1 # numeric identifiers rank lower than alphanumeric + elif [[ "$id2" =~ ^[0-9]+$ ]]; then + return 0 + else + [[ "$id1" > "$id2" ]] && return 0 + [[ "$id1" < "$id2" ]] && return 1 + fi + done return 1 } From feb7ae8c8d07e85fbff99da35260eaa35df84ac7 Mon Sep 17 00:00:00 2001 From: danilaverbena Date: Mon, 27 Jul 2026 01:33:21 +0300 Subject: [PATCH 2/4] fix(arcup): strip build metadata up front and split prerelease without glob Address review feedback on #212: - Strip +build metadata before the '-' split so build metadata containing a hyphen (e.g. 1.0.0+build-123) is not misread as a prerelease. - Split prerelease identifiers with `read -ra` (no pathname/glob expansion) instead of unquoted array assignment. --- arcup/arcup | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/arcup/arcup b/arcup/arcup index 4dff8dc..b6510ea 100755 --- a/arcup/arcup +++ b/arcup/arcup @@ -261,18 +261,17 @@ download_error() { version_gt() { [ "$1" = "$2" ] && return 1 - # Remove 'v' prefix if present - local v1="${1#v}" - local v2="${2#v}" + # Remove 'v' prefix and strip build metadata (+...) up front. SemVer ignores + # build metadata for precedence, and stripping it first prevents a '-' inside + # build metadata (e.g. 1.0.0+build-1) from being misread as a prerelease. + local v1="${1#v}"; v1="${v1%%+*}" + local v2="${2#v}"; v2="${v2%%+*}" - # Split into core "major.minor.patch" and prerelease; drop build metadata (+...), - # which SemVer ignores for precedence. + # Split into core "major.minor.patch" and optional prerelease. local core1="${v1%%-*}" pre1="" [ "$v1" = "$core1" ] || pre1="${v1#*-}" local core2="${v2%%-*}" pre2="" [ "$v2" = "$core2" ] || pre2="${v2#*-}" - core1="${core1%%+*}"; core2="${core2%%+*}" - pre1="${pre1%%+*}"; pre2="${pre2%%+*}" local major1 minor1 patch1 major2 minor2 patch2 IFS=. read -r major1 minor1 patch1 < Date: Mon, 27 Jul 2026 01:33:22 +0300 Subject: [PATCH 3/4] test(arcup): add version_gt SemVer precedence test suite 22 assertions covering the #205 bug cases, the SemVer 11.4 precedence chain, build-metadata handling, and the build-metadata-with-hyphen edge case. Sources arcup with ARCUP_SKIP_MAIN=1. Run: bash arcup/version_gt_test.sh --- arcup/version_gt_test.sh | 76 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 arcup/version_gt_test.sh diff --git a/arcup/version_gt_test.sh b/arcup/version_gt_test.sh new file mode 100644 index 0000000..47204e3 --- /dev/null +++ b/arcup/version_gt_test.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# Tests for version_gt() in ./arcup (SemVer precedence, semver.org section 11). +# +# Usage: +# bash arcup/version_gt_test.sh +# +# It sources ./arcup with ARCUP_SKIP_MAIN=1 so only the function definitions load +# (the installer's main flow is skipped) and then exercises version_gt(). + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export ARCUP_SKIP_MAIN=1 +# shellcheck source=./arcup disable=SC1091 +source "$HERE/arcup" +# arcup runs under `set -euo pipefail`; relax it so the harness controls flow. +set +eu +o pipefail 2>/dev/null || true + +pass=0 +fail=0 + +# assert_gt A B -> expect version_gt to report A > B (exit 0) +assert_gt() { + if version_gt "$1" "$2"; then + pass=$((pass + 1)) + else + echo "FAIL: expected '$1' > '$2'" + fail=$((fail + 1)) + fi +} + +# assert_le A B -> expect version_gt to report NOT A > B (exit non-zero) +assert_le() { + if version_gt "$1" "$2"; then + echo "FAIL: expected NOT '$1' > '$2'" + fail=$((fail + 1)) + else + pass=$((pass + 1)) + fi +} + +# Reported bug (#205): prerelease vs stable and prerelease vs prerelease +assert_gt "0.3.0" "0.3.0-rc.1" +assert_le "0.3.0-rc.1" "0.3.0" +assert_gt "0.3.0-rc.2" "0.3.0-rc.1" +assert_le "0.3.0-rc.1" "0.3.0-rc.2" + +# Core major.minor.patch comparisons (with and without 'v' prefix) +assert_gt "1.0.0" "0.9.9" +assert_le "0.9.9" "1.0.0" +assert_gt "v1.2.4" "v1.2.3" +assert_le "1.2.3" "1.2.3" +assert_gt "1.3.0" "1.2.9" +assert_gt "2.0.0-rc.1" "1.9.9" + +# Full SemVer 11.4 precedence chain: +# 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta +# < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 +assert_gt "1.0.0-alpha.1" "1.0.0-alpha" +assert_gt "1.0.0-alpha.beta" "1.0.0-alpha.1" +assert_gt "1.0.0-beta" "1.0.0-alpha.beta" +assert_gt "1.0.0-beta.2" "1.0.0-beta" +assert_gt "1.0.0-beta.11" "1.0.0-beta.2" +assert_gt "1.0.0-rc.1" "1.0.0-beta.11" +assert_gt "1.0.0" "1.0.0-rc.1" + +# Build metadata is ignored for precedence +assert_le "1.0.0+build.9" "1.0.0+build.1" +assert_le "1.0.0" "1.0.0+build.1" + +# Build metadata containing a hyphen must NOT be treated as a prerelease +assert_le "1.0.0+build-123" "1.0.0" +assert_le "1.0.0" "1.0.0+build-123" +assert_gt "1.0.1+build-1" "1.0.0" + +echo "version_gt: ${pass} passed, ${fail} failed" +[ "${fail}" -eq 0 ] From dbbd86b2a85cc3e8e05b48e81f3cce4598b5e8c4 Mon Sep 17 00:00:00 2001 From: danilaverbena Date: Mon, 27 Jul 2026 15:00:07 +0300 Subject: [PATCH 4/4] test(arcup): cover prerelease + build metadata combinations Per review on #212: exercise the path where a prerelease and build metadata are present simultaneously (build metadata is ignored for precedence). --- arcup/version_gt_test.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/arcup/version_gt_test.sh b/arcup/version_gt_test.sh index 47204e3..4318e7d 100644 --- a/arcup/version_gt_test.sh +++ b/arcup/version_gt_test.sh @@ -72,5 +72,11 @@ assert_le "1.0.0+build-123" "1.0.0" assert_le "1.0.0" "1.0.0+build-123" assert_gt "1.0.1+build-1" "1.0.0" +# Prerelease combined with build metadata: build metadata is ignored, so +# precedence is decided purely by the prerelease identifiers. +assert_le "1.0.0-rc.1+build-123" "1.0.0-rc.1" +assert_le "1.0.0-rc.1" "1.0.0-rc.1+build-123" +assert_gt "1.0.0-rc.2+build-1" "1.0.0-rc.1+build-99" + echo "version_gt: ${pass} passed, ${fail} failed" [ "${fail}" -eq 0 ]