diff --git a/CHANGELOG.md b/CHANGELOG.md index 610af1b..dc7ec41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.1.9] — 2026-06-01 + +### Fixed + +- **Prerelease versions with multi-digit numeric identifiers compared as strings, so `rc.10` looked older than `rc.9`.** `ComparePrerelease` used `string.CompareOrdinal`, which orders `"rc.9"` after `"rc.10"` character-by-character (`'9'` > `'1'`). A CLI on `0.5.0-rc.9` running `update --prerelease` against a `0.5.0-rc.10` release therefore reported "Already up to date." Comparison now follows Semantic Versioning §11: dot-separated prerelease identifiers compare left to right, numeric identifiers compare numerically, numeric ranks below alphanumeric, and a longer identifier set outranks a shorter one with an equal prefix. + +### Changed + +- Bumped NuGet dependencies to their latest versions: `Microsoft.Extensions.DependencyInjection.Abstractions` and `Microsoft.Extensions.Http` 10.0.5 → 10.0.8, `Microsoft.SourceLink.GitHub` 8.0.0 → 10.0.300, and the test stack (`Microsoft.NET.Test.Sdk` 17.11.1 → 18.6.0, `xunit` 2.9.2 → 2.9.3, `xunit.runner.visualstudio` 2.8.2 → 3.1.5, `coverlet.collector` 6.0.2 → 10.0.1). + +--- + ## [0.1.8] — 2026-05-27 ### Added diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj b/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj index 98968c8..83b231a 100644 --- a/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj +++ b/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj @@ -11,7 +11,7 @@ NextIteration.SpectreConsole.SelfUpdate - 0.1.8 + 0.1.9 Stuart Meeks Self-update for Spectre.Console CLIs: pluggable update sources (GitHub Releases over HTTP, GitHub Releases via gh CLI for private repos, generic HTTPS manifest, custom), SHA-256 verification, atomic file swap, and a drop-in `update` command. true @@ -39,11 +39,11 @@ - - + + - + diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateChecker.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateChecker.cs index ae04d08..64c2234 100644 --- a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateChecker.cs +++ b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateChecker.cs @@ -175,6 +175,31 @@ internal static int ComparePrerelease(string? current, string? latest) if (current is null && latest is null) return 0; if (current is null) return 1; // no-prerelease > prerelease, so current is newer if (latest is null) return -1; // current has prerelease, latest does not → current is older + + // Semver §11: compare dot-separated identifiers left to right. + // Numeric identifiers compare numerically; numeric is always lower + // than alphanumeric; otherwise compare lexically (ASCII order). A + // larger set of identifiers outranks a smaller one when all preceding + // identifiers are equal. + var cParts = current.Split('.'); + var lParts = latest.Split('.'); + var shared = Math.Min(cParts.Length, lParts.Length); + for (var i = 0; i < shared; i++) + { + var cmp = ComparePrereleaseIdentifier(cParts[i], lParts[i]); + if (cmp != 0) return cmp; + } + return cParts.Length.CompareTo(lParts.Length); + } + + private static int ComparePrereleaseIdentifier(string current, string latest) + { + var cNumeric = long.TryParse(current, out var cn); + var lNumeric = long.TryParse(latest, out var ln); + + if (cNumeric && lNumeric) return cn.CompareTo(ln); + if (cNumeric) return -1; // numeric identifiers rank lower than alphanumeric + if (lNumeric) return 1; return string.CompareOrdinal(current, latest); } diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj index a2de16c..2b27af2 100644 --- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj +++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj @@ -22,13 +22,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Pipeline/UpdateCheckerVersionTests.cs b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Pipeline/UpdateCheckerVersionTests.cs index 5aa20ac..f8475c9 100644 --- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Pipeline/UpdateCheckerVersionTests.cs +++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Pipeline/UpdateCheckerVersionTests.cs @@ -26,6 +26,17 @@ public void IsNewer_when_numeric_versions_compares_correctly(string current, str [InlineData("1.0.0-beta", "1.0.0-alpha", false)] // beta > alpha lexicographically [InlineData("1.0.0-beta.1", "1.0.0-beta.2", true)] [InlineData("1.0.0-beta", "1.0.0-beta", false)] + // Numeric prerelease identifiers compare numerically, not lexically. + [InlineData("0.5.0-rc.9", "0.5.0-rc.10", true)] // rc.10 is newer than rc.9 + [InlineData("0.5.0-rc.10", "0.5.0-rc.9", false)] // rc.9 is not newer than rc.10 + [InlineData("1.0.0-alpha.2", "1.0.0-alpha.10", true)] + [InlineData("1.0.0-rc.1", "1.0.0-rc.1", false)] + // Fewer identifiers rank lower when the shared prefix is equal. + [InlineData("1.0.0-beta", "1.0.0-beta.1", true)] + [InlineData("1.0.0-beta.1", "1.0.0-beta", false)] + // Numeric identifiers rank lower than alphanumeric ones. + [InlineData("1.0.0-1", "1.0.0-alpha", true)] + [InlineData("1.0.0-alpha", "1.0.0-1", false)] public void IsNewer_with_semver_prerelease_compares_correctly(string current, string latest, bool expected) { Assert.Equal(expected, UpdateChecker.IsNewer(current, latest));