From 2185240cb0c3c9d6a514076a82184b3e97d61d3f Mon Sep 17 00:00:00 2001 From: iammukeshm Date: Thu, 28 May 2026 02:07:25 +0530 Subject: [PATCH] fix(cli): correct update-check version comparison + support --version `fsh info` and `fsh new`'s update check compared versions with string inequality, and the current version carries a `+gitsha` build-metadata suffix, so they always reported "update available" -- even when the local build was NEWER than the latest published prerelease (10.0.0 vs 10.0.0-rc.2, and template 10.0.0 vs 2.0.4-rc). Added a small semver-aware VersionComparer.IsNewer (drops build metadata, ranks a stable release above a prerelease of the same core) and used it in both places, so the hint only shows for a genuine upgrade. Also wired config.SetApplicationVersion so `fsh --version` prints the version instead of erroring "Did you forget the command?". Found while smoke-testing the CLI for release. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Tools/CLI/Commands/InfoCommand.cs | 16 +++--- src/Tools/CLI/Commands/NewCommand.cs | 2 +- .../CLI/Infrastructure/VersionComparer.cs | 55 +++++++++++++++++++ src/Tools/CLI/Program.cs | 8 +++ 4 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 src/Tools/CLI/Infrastructure/VersionComparer.cs diff --git a/src/Tools/CLI/Commands/InfoCommand.cs b/src/Tools/CLI/Commands/InfoCommand.cs index 745eaa3437..962de7c1ee 100644 --- a/src/Tools/CLI/Commands/InfoCommand.cs +++ b/src/Tools/CLI/Commands/InfoCommand.cs @@ -41,10 +41,10 @@ protected override async Task ExecuteAsync(CommandContext context, Cancella // Latest CLI if (latestCli is not null) { - bool isUpToDate = latestCli == currentVersion; - table.AddRow("Latest CLI", isUpToDate - ? $"[{FshConstants.SuccessColor}]{latestCli} (up to date)[/]" - : $"[{FshConstants.WarningColor}]{latestCli} (update available — run 'fsh update')[/]"); + bool updateAvailable = VersionComparer.IsNewer(latestCli, currentVersion); + table.AddRow("Latest CLI", updateAvailable + ? $"[{FshConstants.WarningColor}]{latestCli} (update available — run 'fsh update')[/]" + : $"[{FshConstants.SuccessColor}]{latestCli} (up to date)[/]"); } else { @@ -61,10 +61,10 @@ protected override async Task ExecuteAsync(CommandContext context, Cancella if (latestTemplate is not null) { - bool isUpToDate = latestTemplate == templateVersion; - table.AddRow("Latest Template", isUpToDate - ? $"[{FshConstants.SuccessColor}]{latestTemplate} (up to date)[/]" - : $"[{FshConstants.WarningColor}]{latestTemplate} (update available)[/]"); + bool updateAvailable = VersionComparer.IsNewer(latestTemplate, templateVersion); + table.AddRow("Latest Template", updateAvailable + ? $"[{FshConstants.WarningColor}]{latestTemplate} (update available)[/]" + : $"[{FshConstants.SuccessColor}]{latestTemplate} (up to date)[/]"); } // .NET SDK diff --git a/src/Tools/CLI/Commands/NewCommand.cs b/src/Tools/CLI/Commands/NewCommand.cs index 7e91ed7daa..e62d90df8e 100644 --- a/src/Tools/CLI/Commands/NewCommand.cs +++ b/src/Tools/CLI/Commands/NewCommand.cs @@ -423,7 +423,7 @@ private static async Task CheckForUpdatesAsync(CancellationToken cancellationTok string currentVersion = typeof(NewCommand).Assembly.GetName().Version?.ToString(3) ?? "0.0.0"; - if (latest is not null && latest != currentVersion) + if (VersionComparer.IsNewer(latest, currentVersion)) { AnsiConsole.WriteLine(); AnsiConsole.MarkupLine($"[{FshConstants.WarningColor}]A newer version of FSH CLI is available: {latest} (current: {currentVersion})[/]"); diff --git a/src/Tools/CLI/Infrastructure/VersionComparer.cs b/src/Tools/CLI/Infrastructure/VersionComparer.cs new file mode 100644 index 0000000000..37d41b73a5 --- /dev/null +++ b/src/Tools/CLI/Infrastructure/VersionComparer.cs @@ -0,0 +1,55 @@ +namespace FSH.CLI.Infrastructure; + +/// +/// Minimal semantic-version comparison for the CLI's "update available" hints. +/// Purpose-built so a locally-built CLI/template that is AHEAD of what's published +/// (e.g. a 10.0.0 dev build vs a 10.0.0-rc.2 on NuGet) never nags about a phantom +/// update — the old code compared with string inequality, which also tripped over +/// the `+gitsha` build-metadata suffix on the informational version. +/// +internal static class VersionComparer +{ + /// + /// True only when is a strictly higher semantic + /// version than . Build metadata (+...) is + /// ignored; a stable release outranks any pre-release of the same core + /// version (per SemVer precedence). + /// + internal static bool IsNewer(string? latest, string? current) + { + if (string.IsNullOrWhiteSpace(latest)) return false; + if (string.IsNullOrWhiteSpace(current)) return true; + + (Version latestCore, string latestPre) = Parse(latest); + (Version currentCore, string currentPre) = Parse(current); + + int coreComparison = latestCore.CompareTo(currentCore); + if (coreComparison != 0) return coreComparison > 0; + + // Same core version: a stable build (no pre-release tag) ranks above any + // pre-release; between two pre-releases, compare the tags ordinally. + bool latestStable = latestPre.Length == 0; + bool currentStable = currentPre.Length == 0; + if (latestStable && currentStable) return false; // identical + if (latestStable) return true; // stable > pre-release + if (currentStable) return false; // pre-release < stable + return string.CompareOrdinal(latestPre, currentPre) > 0; // both pre-release + } + + // Splits "10.0.0-rc.2+abc123" into (Version 10.0.0, "rc.2"), dropping build metadata. + private static (Version Core, string PreRelease) Parse(string version) + { + int plus = version.IndexOf('+', StringComparison.Ordinal); + if (plus >= 0) version = version[..plus]; + + string pre = string.Empty; + int dash = version.IndexOf('-', StringComparison.Ordinal); + if (dash >= 0) + { + pre = version[(dash + 1)..]; + version = version[..dash]; + } + + return (Version.TryParse(version, out Version? core) ? core : new Version(0, 0, 0), pre); + } +} diff --git a/src/Tools/CLI/Program.cs b/src/Tools/CLI/Program.cs index 84d52e1f4d..82d17c2205 100644 --- a/src/Tools/CLI/Program.cs +++ b/src/Tools/CLI/Program.cs @@ -1,11 +1,19 @@ +using System.Reflection; using FSH.CLI.Commands; using Spectre.Console.Cli; +// Strip the +gitsha build-metadata suffix so `fsh --version` prints a clean version. +var cliVersion = Assembly.GetExecutingAssembly() + .GetCustomAttribute()?.InformationalVersion?.Split('+')[0] + ?? Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) + ?? "unknown"; + var app = new CommandApp(); app.Configure(config => { config.SetApplicationName("fsh"); + config.SetApplicationVersion(cliVersion); config.AddCommand("new") .WithDescription("Create a new FullStackHero .NET project.")