diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index cb3e95aa..0cbccaf6 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -111,7 +111,9 @@ sealed class Program var releases = sp.GetRequiredService(); var informational = Assembly.GetEntryAssembly()? .GetCustomAttribute()?.InformationalVersion; - // Strip MinVer build metadata ("+sha") and any prerelease suffix for the update-compare. + // Strip MinVer build metadata ("+sha") only — keep the prerelease suffix + // (e.g. "-alpha.0.14"), VersionComparer needs it to tell a dev build on + // main apart from a tagged release of the same numeric core. var version = (informational ?? "0.0.0").Split('+')[0]; return new UpdateCheckService(releases, version); }); diff --git a/src/ClaudeDo.Releases/VersionComparer.cs b/src/ClaudeDo.Releases/VersionComparer.cs index 68d9a35b..5efc87a0 100644 --- a/src/ClaudeDo.Releases/VersionComparer.cs +++ b/src/ClaudeDo.Releases/VersionComparer.cs @@ -10,7 +10,17 @@ public static class VersionComparer | !Version.TryParse(CoreVersion(current), out var cv); if (unparseable) return new VersionCompareResult(false, true); - return new VersionCompareResult(lv > cv, false); + if (lv != cv) return new VersionCompareResult(lv > cv, false); + + // Same numeric core: a MinVer dev build sitting on main between two tags + // reports a prerelease of its own guessed *next* version (e.g. after tag + // v1.4.0, main computes "1.4.1-alpha.0.12" — already ahead of the tag by + // core, handled above). But when a real release is tagged at exactly that + // guessed version, the tag itself has no prerelease suffix while a still- + // running dev build does — that dev build must still count as older, or a + // genuine new release would be silently missed. + var isNewer = HasPrerelease(current) && !HasPrerelease(latest); + return new VersionCompareResult(isNewer, false); } // Reduce a tag/version to its numeric core: drop a leading "v", MinVer build @@ -18,4 +28,7 @@ public static class VersionComparer // 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]; + + private static bool HasPrerelease(string value) + => (value ?? "").TrimStart('v', 'V').Split('+')[0].Contains('-'); } diff --git a/tests/ClaudeDo.Releases.Tests/VersionComparerTests.cs b/tests/ClaudeDo.Releases.Tests/VersionComparerTests.cs index 856d1df9..7d80e266 100644 --- a/tests/ClaudeDo.Releases.Tests/VersionComparerTests.cs +++ b/tests/ClaudeDo.Releases.Tests/VersionComparerTests.cs @@ -17,6 +17,15 @@ public class VersionComparerTests [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 + // A dev build sitting on main computes a MinVer prerelease of its own guessed next + // version, ahead of the last tag by core -> no banner (real-world repro: latest + // published tag v2.9.0, worktree 14 commits ahead builds "2.9.1-alpha.0.14+sha"). + [InlineData("2.9.0", "2.9.1-alpha.0.14", false, false)] + // A genuine new release lands at exactly the version a running dev build had + // already guessed -> the release must still win over the still-running prerelease. + [InlineData("1.4.1", "1.4.1-alpha.0.12", true, false)] + // An installed real release exactly matching the latest tag stays "up to date". + [InlineData("1.4.1", "1.4.1", false, false)] public void Compare_ParseableVersions(string latest, string current, bool expectedNewer, bool expectedUnparseable) { var result = VersionComparer.Compare(latest, current);