diff --git a/src/ClaudeDo.Releases/VersionComparer.cs b/src/ClaudeDo.Releases/VersionComparer.cs index e38d1fc..68d9a35 100644 --- a/src/ClaudeDo.Releases/VersionComparer.cs +++ b/src/ClaudeDo.Releases/VersionComparer.cs @@ -6,13 +6,16 @@ public static class VersionComparer { public static VersionCompareResult Compare(string latest, string current) { - var latestTrimmed = (latest ?? "").TrimStart('v', 'V'); - var currentTrimmed = (current ?? "").TrimStart('v', 'V'); - - var unparseable = !Version.TryParse(latestTrimmed, out var lv) - | !Version.TryParse(currentTrimmed, out var cv); + var unparseable = !Version.TryParse(CoreVersion(latest), out var lv) + | !Version.TryParse(CoreVersion(current), out var cv); if (unparseable) return new VersionCompareResult(false, true); return new VersionCompareResult(lv > cv, false); } + + // Reduce a tag/version to its numeric core: drop a leading "v", MinVer build + // metadata ("+sha"), and any SemVer prerelease suffix ("-alpha") — none of + // which System.Version can parse. So "v1.0.2-alpha+abc" -> "1.0.2". + private static string CoreVersion(string value) + => (value ?? "").TrimStart('v', 'V').Split('+')[0].Split('-')[0]; } diff --git a/tests/ClaudeDo.Releases.Tests/VersionComparerTests.cs b/tests/ClaudeDo.Releases.Tests/VersionComparerTests.cs index 5aba89f..856d1df 100644 --- a/tests/ClaudeDo.Releases.Tests/VersionComparerTests.cs +++ b/tests/ClaudeDo.Releases.Tests/VersionComparerTests.cs @@ -9,6 +9,14 @@ public class VersionComparerTests [InlineData("v0.2.0", "0.1.0", true, false)] [InlineData("0.2.0", "v0.1.0", true, false)] [InlineData("1.0.0.0", "0.99.99.99", true, false)] + // Prerelease and build-metadata suffixes are stripped to the numeric core + // before comparing (System.Version can't parse them otherwise). + [InlineData("0.2.0-beta", "0.1.0", true, false)] + [InlineData("0.2.0", "0.1.0-alpha", true, false)] + [InlineData("1.2.0", "1.0.2-alpha", true, false)] // real-world: installed 1.0.2-alpha -> 1.2.0 + [InlineData("v1.0.2-alpha+1c764dae", "1.0.0", true, false)] // v-prefix + prerelease + build metadata combined + [InlineData("1.0.2-alpha+abc", "1.0.2-alpha", false, false)] // same core -> not newer + [InlineData("1.2.0-rc1", "1.2.0", false, false)] // prerelease of an already-installed release public void Compare_ParseableVersions(string latest, string current, bool expectedNewer, bool expectedUnparseable) { var result = VersionComparer.Compare(latest, current); @@ -17,9 +25,8 @@ public class VersionComparerTests } [Theory] - [InlineData("0.2.0-beta", "0.1.0")] - [InlineData("0.2.0", "0.1.0-alpha")] [InlineData("garbage", "0.1.0")] + [InlineData("0.2.0", "garbage")] [InlineData("", "0.1.0")] public void Compare_UnparseableReturnsNotNewer(string latest, string current) {