Skip to content

Bug: arcup SemVer comparison treats prerelease and stable versions as equal #205

Description

@mssystem1

Summary

The version_gt() function removes the prerelease suffix from both versions before comparing them:

ver1="${ver1%%-*}"
ver2="${ver2%%-*}"

This causes prerelease and stable versions with the same major, minor, and patch numbers to be treated as equal.

For example:

0.3.0-rc.1
0.3.0

Both are normalized internally to:

0.3.0

As a result, arcup does not recognize the stable release as newer than the release candidate.

Repository

circlefin/arc-node

Create issue:

https://github.com/circlefin/arc-node/issues/new

Affected file

arcup/arcup

arc-node/arcup/arcup

Lines 261 to 286 in 745ba4e

version_gt() {
[ "$1" = "$2" ] && return 1
# Remove 'v' prefix if present
local ver1="${1#v}"
local ver2="${2#v}"
ver1="${ver1%%-*}"
ver2="${ver2%%-*}"
IFS=. read -r major1 minor1 patch1 <<EOF
$ver1
EOF
IFS=. read -r major2 minor2 patch2 <<EOF
$ver2
EOF
[ "$major1" -gt "$major2" ] && return 0
[ "$major1" -lt "$major2" ] && return 1
[ "$minor1" -gt "$minor2" ] && return 0
[ "$minor1" -lt "$minor2" ] && return 1
[ "$patch1" -gt "$patch2" ] && return 0
[ "$patch1" -lt "$patch2" ] && return 1
return 1
}

The function is used by the installer update checks:

arc-node/arcup/arcup

Lines 413 to 430 in 745ba4e

local response
local remote_version
response=$(curl_with_headers -fsSL "$ARCUP_BIN_URL" 2>/dev/null || true)
remote_version=$(printf '%s\n' "$response" | awk -F'"' '/^ARCUP_INSTALLER_VERSION=/ {print $2; exit}')
if [[ -z "$remote_version" ]]; then
return
fi
if version_gt "$remote_version" "$ARCUP_INSTALLER_VERSION"; then
echo ""
warn "Your arcup version ($ARCUP_INSTALLER_VERSION) is outdated."
warn "Latest version is $remote_version."
warn "Run 'arcup --self-update' to update to the latest version."
echo ""
fi
}

arc-node/arcup/arcup

Lines 454 to 462 in 745ba4e

exit 0
fi
info "Updating from $ARCUP_INSTALLER_VERSION to $remote_version..."
if cp "$_ARCUP_TMP_FILE" "$ARCUP_BIN_PATH" 2>/dev/null; then
chmod 755 "$ARCUP_BIN_PATH"
else
error "Failed to update arcup at '$ARCUP_BIN_PATH' — ensure it is writable"
fi

Steps to reproduce

Source the script without running the main installation flow:

export ARCUP_SKIP_MAIN=1
source ./arcup/arcup

Compare a stable version with a release candidate:

version_gt "0.3.0" "0.3.0-rc.1"
echo $?

The function returns a non-zero status, meaning 0.3.0 is not considered greater.

The same issue can be demonstrated in the opposite direction:

version_gt "0.3.0-rc.2" "0.3.0-rc.1"
echo $?

Both prereleases are reduced to 0.3.0, so the newer release candidate is also not detected.

Expected behavior

The comparison should follow Semantic Versioning precedence:

0.3.0-rc.1 < 0.3.0-rc.2 < 0.3.0

Therefore:

version_gt "0.3.0" "0.3.0-rc.1"

should return success.

Likewise:

version_gt "0.3.0-rc.2" "0.3.0-rc.1"

should return success.

Actual behavior

All versions sharing the same major, minor, and patch numbers are treated as equal after their prerelease suffixes are removed.

Examples incorrectly treated as equivalent:

1.0.0-alpha
1.0.0-beta
1.0.0-rc.1
1.0.0

Impact

Users running a prerelease version of arcup may not be notified about or upgraded to the corresponding stable release.

Users may also fail to receive newer prerelease builds when only the prerelease identifier changes.

This can leave users on:

  • release-candidate code after a stable release is available;
  • older prerelease builds;
  • installer versions containing bugs already fixed in a later prerelease or stable version.

Root cause

The implementation compares only numeric core components:

major.minor.patch

and intentionally discards everything following -, even though the accepted version format explicitly allows prerelease suffixes:

^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$

The accepted input format and comparison behavior are therefore inconsistent.

Suggested fix

Use a SemVer-aware comparison implementation.

At minimum:

  1. Compare major, minor, and patch numerically.
  2. When the core versions are equal:
    • a stable version is greater than a prerelease;
    • compare dot-separated prerelease identifiers;
    • compare numeric identifiers numerically;
    • compare non-numeric identifiers lexically;
    • numeric identifiers have lower precedence than non-numeric identifiers.

Alternatively, use a small dedicated SemVer utility already available on the target platforms or restrict installer versions to stable releases only.

Example expected comparisons:

1.0.0 > 1.0.0-rc.1
1.0.0-rc.2 > 1.0.0-rc.1
1.0.0-beta.11 > 1.0.0-beta.2
1.0.1 > 1.0.0
2.0.0 > 1.99.99

Tests

Add tests for:

version_gt "1.0.0" "1.0.0-rc.1"
version_gt "1.0.0-rc.2" "1.0.0-rc.1"
version_gt "1.0.0-beta.11" "1.0.0-beta.2"
version_gt "1.0.1" "1.0.0"
version_gt "2.0.0" "1.99.99"

Also verify the inverse cases return false:

version_gt "1.0.0-rc.1" "1.0.0"
version_gt "1.0.0-rc.1" "1.0.0-rc.2"
version_gt "1.0.0" "1.0.0"

Checklist

  • Preserve prerelease identifiers during comparison
  • Treat stable releases as newer than matching prereleases
  • Compare prerelease identifiers according to SemVer
  • Add unit tests for prerelease ordering
  • Verify update notifications from RC to stable
  • Verify self-update from older to newer prereleases

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions