From 277d8ef4e8b714121ea25a59b6e15bca0fa77236 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 18 Jul 2026 17:22:11 +1000 Subject: [PATCH 1/3] Version Overrides --- readme.md | 38 +++++ readme.source.md | 38 +++++ src/PackageUpdate/Updater.cs | 132 ++++++++++++++---- src/PackageUpdate/nuget.md | 1 + src/Tests/UpdaterTests.cs | 261 +++++++++++++++++++++++++++++++++++ 5 files changed, 440 insertions(+), 30 deletions(-) diff --git a/readme.md b/readme.md index 34cc8ed..e12c093 100644 --- a/readme.md +++ b/readme.md @@ -330,6 +330,44 @@ The next time you run the updater, it will update to the latest version. - Comments and formatting around pinned packages are preserved during updates +## Pre-release Version Overrides + + +### Overview + +Under Central Package Management, a project can reference a different version than the one declared centrally in `Directory.Packages.props` by adding a [`VersionOverride`](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management#overriding-package-versions) attribute to its `PackageReference`. + +A common use case is keeping the packable projects on the **stable** version of a dependency, while a test project tracks its **pre-release**. PackageUpdate maintains both: + +- The central `PackageVersion` follows the normal rule (a stable version is only moved to a newer stable). +- Any `VersionOverride` is moved to the latest available version, **including pre-releases**. + + +### Example + +`Directory.Packages.props` stays on the stable: + +```xml + +``` + +`Tests/Tests.csproj` tracks the pre-release: + +```xml + +``` + +When a newer pre-release (or a newer version generally) is published, only the override in the test project is advanced; the central stable entry is left untouched. + + +### Behavior + +- `VersionOverride` entries are updated in all `*.csproj` files under each solution directory. +- Pre-release versions are always considered for overrides, so an override stays on the latest build rather than getting stuck once it graduates to a stable. +- A `VersionOverride` is only skipped when the `PackageReference` itself has `Pinned="true"`. Pinning the central `PackageVersion` does not pin the override, so the deployed projects can stay fixed while a test project keeps floating. +- The `--package` filter also applies to override updates. + + ## Automatic Package Migration diff --git a/readme.source.md b/readme.source.md index 955d546..fddd73c 100644 --- a/readme.source.md +++ b/readme.source.md @@ -305,6 +305,44 @@ The next time you run the updater, it will update to the latest version. - Comments and formatting around pinned packages are preserved during updates +## Pre-release Version Overrides + + +### Overview + +Under Central Package Management, a project can reference a different version than the one declared centrally in `Directory.Packages.props` by adding a [`VersionOverride`](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management#overriding-package-versions) attribute to its `PackageReference`. + +A common use case is keeping the packable projects on the **stable** version of a dependency, while a test project tracks its **pre-release**. PackageUpdate maintains both: + +- The central `PackageVersion` follows the normal rule (a stable version is only moved to a newer stable). +- Any `VersionOverride` is moved to the latest available version, **including pre-releases**. + + +### Example + +`Directory.Packages.props` stays on the stable: + +```xml + +``` + +`Tests/Tests.csproj` tracks the pre-release: + +```xml + +``` + +When a newer pre-release (or a newer version generally) is published, only the override in the test project is advanced; the central stable entry is left untouched. + + +### Behavior + +- `VersionOverride` entries are updated in all `*.csproj` files under each solution directory. +- Pre-release versions are always considered for overrides, so an override stays on the latest build rather than getting stuck once it graduates to a stable. +- A `VersionOverride` is only skipped when the `PackageReference` itself has `Pinned="true"`. Pinning the central `PackageVersion` does not pin the override, so the deployed projects can stay fixed while a test project keeps floating. +- The `--package` filter also applies to override updates. + + ## Automatic Package Migration diff --git a/src/PackageUpdate/Updater.cs b/src/PackageUpdate/Updater.cs index 71f3a99..eca1336 100644 --- a/src/PackageUpdate/Updater.cs +++ b/src/PackageUpdate/Updater.cs @@ -2,6 +2,7 @@ { static ConcurrentDictionary<(string Package, NuGetVersion Version), IPackageSearchMetadata?> metadataCache = new(PackageCacheKeyComparer.Instance); static ConcurrentDictionary<(string Package, NuGetVersion CurrentVersion), IPackageSearchMetadata?> latestVersionCache = new(PackageCacheKeyComparer.Instance); + static ConcurrentDictionary<(string Package, NuGetVersion CurrentVersion), IPackageSearchMetadata?> latestPrereleaseVersionCache = new(PackageCacheKeyComparer.Instance); public static async Task Update( SourceCacheContext cache, @@ -110,6 +111,20 @@ public static async Task Update( Log.Information("Updated {Package}: {NuGetVersion} -> {LatestVersion}", package.Package, currentVersion, latestVersion); } + await SaveXml(directoryPackagesPropsPath, xml, newLine, hasTrailingNewline); + + // Update PackageReference entries in csproj files for migrated packages + if (migrations.Count > 0) + { + await UpdateCsprojFiles(directory, migrations); + } + + // Update VersionOverride entries in csproj files (e.g. test projects tracking a pre-release) + await UpdateVersionOverrides(directory, packageName, sources, cache); + } + + static async Task SaveXml(string path, XDocument xml, string newLine, bool hasTrailingNewline) + { var xmlSettings = new XmlWriterSettings { OmitXmlDeclaration = true, @@ -120,7 +135,7 @@ public static async Task Update( Async = true }; - await using (var writer = XmlWriter.Create(directoryPackagesPropsPath, xmlSettings)) + await using (var writer = XmlWriter.Create(path, xmlSettings)) { await xml.SaveAsync(writer, Cancel.None); } @@ -128,13 +143,7 @@ public static async Task Update( // Match the original trailing newline convention if (hasTrailingNewline) { - await File.AppendAllTextAsync(directoryPackagesPropsPath, newLine); - } - - // Update PackageReference entries in csproj files for migrated packages - if (migrations.Count > 0) - { - await UpdateCsprojFiles(directory, migrations); + await File.AppendAllTextAsync(path, newLine); } } @@ -180,10 +189,12 @@ public static async Task Update( string package, NuGetVersion currentVersion, List sources, - SourceCacheContext cache) + SourceCacheContext cache, + bool includePrerelease = false) { + var versionCache = includePrerelease ? latestPrereleaseVersionCache : latestVersionCache; var key = (package, currentVersion); - if (latestVersionCache.TryGetValue(key, out var cached)) + if (versionCache.TryGetValue(key, out var cached)) { return cached; } @@ -194,7 +205,7 @@ public static async Task Update( { var (repository, metadataResource) = await RepositoryReader.Read(source); - var condidates = await GetCondidates(package, currentVersion, cache, repository); + var condidates = await GetCondidates(package, currentVersion, cache, repository, includePrerelease); foreach (var candidate in condidates) { @@ -221,7 +232,7 @@ public static async Task Update( } } - latestVersionCache[key] = latestMetadata; + versionCache[key] = latestMetadata; return latestMetadata; } @@ -362,25 +373,86 @@ static async Task UpdateCsprojFiles(string directory, List<(string OldPackage, s if (updated) { - var xmlSettings = new XmlWriterSettings + await SaveXml(csprojPath, csprojXml, newLine, hasTrailingNewline); + } + } + } + + static async Task UpdateVersionOverrides( + string directory, + string? packageName, + List sources, + SourceCacheContext cache) + { + foreach (var csprojPath in EnumerateCsprojFiles(directory)) + { + var (newLine, hasTrailingNewline) = DetectNewLineInfo(csprojPath); + var csprojXml = XDocument.Load(csprojPath); + + var overrides = csprojXml.Descendants("PackageReference") + .Select(element => new + { + Element = element, + Package = element.Attribute("Include")?.Value, + CurrentOverride = element.Attribute("VersionOverride")?.Value, + Pinned = element.Attribute("Pinned")?.Value == "true" + }) + .Where(_ => _.Package != null && + _.CurrentOverride != null && + !_.Pinned) + .ToList(); + + var updated = false; + + foreach (var packageOverride in overrides) + { + // Filter to specific package if requested + if (!string.IsNullOrEmpty(packageName) && + !string.Equals(packageOverride.Package, packageName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (!NuGetVersion.TryParse(packageOverride.CurrentOverride, out var currentVersion)) { - OmitXmlDeclaration = true, - Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), - Indent = true, - IndentChars = " ", - NewLineChars = newLine, - Async = true - }; - - await using (var writer = XmlWriter.Create(csprojPath, xmlSettings)) + continue; + } + + // A VersionOverride is typically used to track a pre-release (e.g. a test project), + // so always consider pre-release versions regardless of the current value. This keeps + // the override on the latest build and avoids getting stuck once it graduates to a stable. + var latestMetadata = await GetLatestVersion( + packageOverride.Package!, + currentVersion, + sources, + cache, + includePrerelease: true); + + if (latestMetadata == null) { - await csprojXml.SaveAsync(writer, Cancel.None); + continue; } - if (hasTrailingNewline) + var latestVersion = latestMetadata.Identity.Version; + + if (latestVersion <= currentVersion) { - await File.AppendAllTextAsync(csprojPath, newLine); + continue; } + + packageOverride.Element.SetAttributeValue("VersionOverride", latestVersion.ToString()); + updated = true; + Log.Information( + "Updated {Package} VersionOverride: {NuGetVersion} -> {LatestVersion} in {File}", + packageOverride.Package, + currentVersion, + latestVersion, + Path.GetFileName(csprojPath)); + } + + if (updated) + { + await SaveXml(csprojPath, csprojXml, newLine, hasTrailingNewline); } } } @@ -424,7 +496,7 @@ static IEnumerable EnumerateCsprojFiles(string directory) } } - static async Task> GetCondidates(string package, NuGetVersion currentVersion, SourceCacheContext cache, SourceRepository repository) + static async Task> GetCondidates(string package, NuGetVersion currentVersion, SourceCacheContext cache, SourceRepository repository, bool includePrerelease) { // Use FindPackageByIdResource to efficiently get version list var findResource = await repository.GetResourceAsync(); @@ -436,16 +508,16 @@ static async Task> GetCondidates(string package, NuGetVersion Cancel.None); return versions - .Where(_ => ShouldConsiderVersion(_, currentVersion)) + .Where(_ => ShouldConsiderVersion(_, currentVersion, includePrerelease)) .OrderDescending() .ToList(); } - static bool ShouldConsiderVersion(NuGetVersion candidate, NuGetVersion current) + static bool ShouldConsiderVersion(NuGetVersion candidate, NuGetVersion current, bool includePrerelease) { - // If current is stable, only consider stable or newer versions + // If current is stable, only consider stable or newer versions, unless pre-releases are explicitly allowed // If current is pre-release, consider any newer version - if (!current.IsPrerelease && candidate.IsPrerelease) + if (!includePrerelease && !current.IsPrerelease && candidate.IsPrerelease) { return false; } diff --git a/src/PackageUpdate/nuget.md b/src/PackageUpdate/nuget.md index a2586be..64b7f88 100644 --- a/src/PackageUpdate/nuget.md +++ b/src/PackageUpdate/nuget.md @@ -34,6 +34,7 @@ If no directory is passed the current directory will be used. * Updates all packages across all solutions in a directory * Respects `Pinned="true"` attribute to skip specific packages + * Updates `VersionOverride` entries (e.g. test projects tracking a pre-release) independently of the central stable version * Automatically migrates deprecated packages to recommended alternatives * Preserves file formatting (newlines, indentation) * Queries all configured NuGet sources diff --git a/src/Tests/UpdaterTests.cs b/src/Tests/UpdaterTests.cs index 09cc6ef..608ea1d 100644 --- a/src/Tests/UpdaterTests.cs +++ b/src/Tests/UpdaterTests.cs @@ -1434,4 +1434,265 @@ public async Task ConcurrentRepositoryReaderAccessDoesNotThrow() await Assert.That(metadataResource).IsNotNull(); } } + + static string nugetConfigOnlyOrg = + """ + + + + + + + + """; + + static async Task<(string props, string csproj)> RunOverrideScenario( + string directoryPackages, + string csproj, + string? package = null) + { + using var cache = new SourceCacheContext + { + RefreshMemoryCache = true + }; + + using var directory = new TempDirectory(); + var nugetConfigPath = Path.Combine(directory, "nuget.config"); + var packagesPath = Path.Combine(directory, "Directory.Packages.props"); + var csprojPath = Path.Combine(directory, "Tests.csproj"); + + await File.WriteAllTextAsync(nugetConfigPath, nugetConfigOnlyOrg); + await File.WriteAllTextAsync(packagesPath, directoryPackages); + await File.WriteAllTextAsync(csprojPath, csproj); + + await Updater.Update(cache, packagesPath, package); + + return ( + await File.ReadAllTextAsync(packagesPath), + await File.ReadAllTextAsync(csprojPath)); + } + + static string? OverrideOf(string csproj, string package) => + XDocument.Parse(csproj) + .Descendants("PackageReference") + .FirstOrDefault(_ => string.Equals(_.Attribute("Include")?.Value, package, StringComparison.OrdinalIgnoreCase)) + ?.Attribute("VersionOverride")?.Value; + + [Test] + public async Task UpdatesVersionOverrideInCsproj() + { + var directoryPackages = + """ + + + + + + """; + + var csproj = + """ + + + + + + """; + + var (props, csprojResult) = await RunOverrideScenario(directoryPackages, csproj); + + // The VersionOverride in the csproj should have moved forward + var updatedOverride = OverrideOf(csprojResult, "Newtonsoft.Json"); + await Assert.That(updatedOverride).IsNotEqualTo("12.0.1"); + await Assert.That(NuGetVersion.TryParse(updatedOverride, out var overrideVersion)).IsTrue(); + await Assert.That(overrideVersion! > NuGetVersion.Parse("12.0.1")).IsTrue(); + + // The central stable version must stay stable + var centralVersion = XDocument.Parse(props) + .Descendants("PackageVersion") + .Single(_ => _.Attribute("Include")?.Value == "Newtonsoft.Json") + .Attribute("Version")!.Value; + await Assert.That(NuGetVersion.Parse(centralVersion).IsPrerelease).IsFalse(); + } + + [Test] + public async Task VersionOverridePreReleaseMovesForward() + { + // Central stays on a stable, the test csproj tracks the pre-release via VersionOverride + var directoryPackages = + """ + + + + + + """; + + var csproj = + """ + + + + + + """; + + var (_, csprojResult) = await RunOverrideScenario(directoryPackages, csproj); + + var updatedOverride = OverrideOf(csprojResult, "Verify"); + await Assert.That(updatedOverride).IsNotEqualTo("1.0.0-beta.1"); + await Assert.That(NuGetVersion.TryParse(updatedOverride, out var overrideVersion)).IsTrue(); + await Assert.That(overrideVersion! > NuGetVersion.Parse("1.0.0-beta.1")).IsTrue(); + } + + [Test] + public async Task VersionOverrideRespectsPackageFilter() + { + var directoryPackages = + """ + + + + + + """; + + var csproj = + """ + + + + + + + """; + + var (_, csprojResult) = await RunOverrideScenario(directoryPackages, csproj, "Newtonsoft.Json"); + + // Targeted package override moves + await Assert.That(OverrideOf(csprojResult, "Newtonsoft.Json")).IsNotEqualTo("12.0.1"); + + // Non-targeted package override is left untouched + await Assert.That(OverrideOf(csprojResult, "Serilog")).IsEqualTo("2.0.0"); + } + + [Test] + public async Task PackageReferenceWithoutVersionOverrideUnchanged() + { + var directoryPackages = + """ + + + + + + """; + + var csproj = + """ + + + + + + """; + + var (_, csprojResult) = await RunOverrideScenario(directoryPackages, csproj); + + // A plain PackageReference must never gain a VersionOverride or Version attribute + await Assert.That(csprojResult).DoesNotContain("VersionOverride"); + await Assert.That(csprojResult).DoesNotContain("Version="); + } + + [Test] + public async Task VersionOverridePreservesCsprojFormatting() + { + var directoryPackages = + """ + + + + + + """; + + var csproj = + """ + + + + + + + + """; + + var (_, csprojResult) = await RunOverrideScenario(directoryPackages, csproj); + + await Assert.That(csprojResult).Contains(""); + await Assert.That(csprojResult).Contains(""); + await Assert.That(OverrideOf(csprojResult, "Newtonsoft.Json")).IsNotEqualTo("12.0.1"); + } + + [Test] + public async Task PinnedVersionOverrideNotUpdated() + { + var directoryPackages = + """ + + + + + + """; + + var csproj = + """ + + + + + + """; + + var (_, csprojResult) = await RunOverrideScenario(directoryPackages, csproj); + + // The override itself is pinned, so it must not move + await Assert.That(OverrideOf(csprojResult, "Newtonsoft.Json")).IsEqualTo("12.0.1"); + await Assert.That(csprojResult).Contains("Pinned=\"true\""); + } + + [Test] + public async Task CentralPinDoesNotPinVersionOverride() + { + var directoryPackages = + """ + + + + + + """; + + var csproj = + """ + + + + + + """; + + var (props, csprojResult) = await RunOverrideScenario(directoryPackages, csproj); + + // The central pin keeps the central version fixed + var centralVersion = XDocument.Parse(props) + .Descendants("PackageVersion") + .Single(_ => _.Attribute("Include")?.Value == "Newtonsoft.Json") + .Attribute("Version")!.Value; + await Assert.That(centralVersion).IsEqualTo("12.0.1"); + + // But the override is only pinned by its own Pinned attribute, so it still advances + await Assert.That(OverrideOf(csprojResult, "Newtonsoft.Json")).IsNotEqualTo("12.0.1"); + } } \ No newline at end of file From 2d682a23743f91baec20f91e48be948f20b70b7e Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 18 Jul 2026 18:02:34 +1000 Subject: [PATCH 2/3] . --- readme.md | 19 +++++++++++++------ readme.source.md | 19 +++++++++++++------ src/PackageUpdate/Updater.cs | 29 ++++++++++++----------------- src/PackageUpdate/nuget.md | 2 +- src/Tests/UpdaterTests.cs | 3 +++ 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/readme.md b/readme.md index e12c093..a595d19 100644 --- a/readme.md +++ b/readme.md @@ -330,21 +330,23 @@ The next time you run the updater, it will update to the latest version. - Comments and formatting around pinned packages are preserved during updates -## Pre-release Version Overrides +## Version Overrides ### Overview Under Central Package Management, a project can reference a different version than the one declared centrally in `Directory.Packages.props` by adding a [`VersionOverride`](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management#overriding-package-versions) attribute to its `PackageReference`. -A common use case is keeping the packable projects on the **stable** version of a dependency, while a test project tracks its **pre-release**. PackageUpdate maintains both: +PackageUpdate keeps these overrides up to date, independently of the central version. Each override is updated using the same rule applied to central versions: -- The central `PackageVersion` follows the normal rule (a stable version is only moved to a newer stable). -- Any `VersionOverride` is moved to the latest available version, **including pre-releases**. +- A stable override is only moved to a newer stable version. +- A pre-release override also considers pre-release versions. ### Example +A common use case is keeping the packable projects on the **stable** version of a dependency, while a test project tracks its **pre-release**. + `Directory.Packages.props` stays on the stable: ```xml @@ -357,13 +359,18 @@ A common use case is keeping the packable projects on the **stable** version of ``` -When a newer pre-release (or a newer version generally) is published, only the override in the test project is advanced; the central stable entry is left untouched. +When a newer pre-release is published, only the override in the test project is advanced; the central stable entry is left untouched. + +Overrides that point at a stable version work the same way, and are moved to newer stable versions: + +```xml + +``` ### Behavior - `VersionOverride` entries are updated in all `*.csproj` files under each solution directory. -- Pre-release versions are always considered for overrides, so an override stays on the latest build rather than getting stuck once it graduates to a stable. - A `VersionOverride` is only skipped when the `PackageReference` itself has `Pinned="true"`. Pinning the central `PackageVersion` does not pin the override, so the deployed projects can stay fixed while a test project keeps floating. - The `--package` filter also applies to override updates. diff --git a/readme.source.md b/readme.source.md index fddd73c..0312d26 100644 --- a/readme.source.md +++ b/readme.source.md @@ -305,21 +305,23 @@ The next time you run the updater, it will update to the latest version. - Comments and formatting around pinned packages are preserved during updates -## Pre-release Version Overrides +## Version Overrides ### Overview Under Central Package Management, a project can reference a different version than the one declared centrally in `Directory.Packages.props` by adding a [`VersionOverride`](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management#overriding-package-versions) attribute to its `PackageReference`. -A common use case is keeping the packable projects on the **stable** version of a dependency, while a test project tracks its **pre-release**. PackageUpdate maintains both: +PackageUpdate keeps these overrides up to date, independently of the central version. Each override is updated using the same rule applied to central versions: -- The central `PackageVersion` follows the normal rule (a stable version is only moved to a newer stable). -- Any `VersionOverride` is moved to the latest available version, **including pre-releases**. +- A stable override is only moved to a newer stable version. +- A pre-release override also considers pre-release versions. ### Example +A common use case is keeping the packable projects on the **stable** version of a dependency, while a test project tracks its **pre-release**. + `Directory.Packages.props` stays on the stable: ```xml @@ -332,13 +334,18 @@ A common use case is keeping the packable projects on the **stable** version of ``` -When a newer pre-release (or a newer version generally) is published, only the override in the test project is advanced; the central stable entry is left untouched. +When a newer pre-release is published, only the override in the test project is advanced; the central stable entry is left untouched. + +Overrides that point at a stable version work the same way, and are moved to newer stable versions: + +```xml + +``` ### Behavior - `VersionOverride` entries are updated in all `*.csproj` files under each solution directory. -- Pre-release versions are always considered for overrides, so an override stays on the latest build rather than getting stuck once it graduates to a stable. - A `VersionOverride` is only skipped when the `PackageReference` itself has `Pinned="true"`. Pinning the central `PackageVersion` does not pin the override, so the deployed projects can stay fixed while a test project keeps floating. - The `--package` filter also applies to override updates. diff --git a/src/PackageUpdate/Updater.cs b/src/PackageUpdate/Updater.cs index eca1336..0f60743 100644 --- a/src/PackageUpdate/Updater.cs +++ b/src/PackageUpdate/Updater.cs @@ -2,7 +2,6 @@ { static ConcurrentDictionary<(string Package, NuGetVersion Version), IPackageSearchMetadata?> metadataCache = new(PackageCacheKeyComparer.Instance); static ConcurrentDictionary<(string Package, NuGetVersion CurrentVersion), IPackageSearchMetadata?> latestVersionCache = new(PackageCacheKeyComparer.Instance); - static ConcurrentDictionary<(string Package, NuGetVersion CurrentVersion), IPackageSearchMetadata?> latestPrereleaseVersionCache = new(PackageCacheKeyComparer.Instance); public static async Task Update( SourceCacheContext cache, @@ -189,12 +188,10 @@ static async Task SaveXml(string path, XDocument xml, string newLine, bool hasTr string package, NuGetVersion currentVersion, List sources, - SourceCacheContext cache, - bool includePrerelease = false) + SourceCacheContext cache) { - var versionCache = includePrerelease ? latestPrereleaseVersionCache : latestVersionCache; var key = (package, currentVersion); - if (versionCache.TryGetValue(key, out var cached)) + if (latestVersionCache.TryGetValue(key, out var cached)) { return cached; } @@ -205,7 +202,7 @@ static async Task SaveXml(string path, XDocument xml, string newLine, bool hasTr { var (repository, metadataResource) = await RepositoryReader.Read(source); - var condidates = await GetCondidates(package, currentVersion, cache, repository, includePrerelease); + var condidates = await GetCondidates(package, currentVersion, cache, repository); foreach (var candidate in condidates) { @@ -232,7 +229,7 @@ static async Task SaveXml(string path, XDocument xml, string newLine, bool hasTr } } - versionCache[key] = latestMetadata; + latestVersionCache[key] = latestMetadata; return latestMetadata; } @@ -418,15 +415,13 @@ static async Task UpdateVersionOverrides( continue; } - // A VersionOverride is typically used to track a pre-release (e.g. a test project), - // so always consider pre-release versions regardless of the current value. This keeps - // the override on the latest build and avoids getting stuck once it graduates to a stable. + // An override follows the same rule as a central version: a stable override only moves + // to a newer stable, a pre-release override also considers pre-releases var latestMetadata = await GetLatestVersion( packageOverride.Package!, currentVersion, sources, - cache, - includePrerelease: true); + cache); if (latestMetadata == null) { @@ -496,7 +491,7 @@ static IEnumerable EnumerateCsprojFiles(string directory) } } - static async Task> GetCondidates(string package, NuGetVersion currentVersion, SourceCacheContext cache, SourceRepository repository, bool includePrerelease) + static async Task> GetCondidates(string package, NuGetVersion currentVersion, SourceCacheContext cache, SourceRepository repository) { // Use FindPackageByIdResource to efficiently get version list var findResource = await repository.GetResourceAsync(); @@ -508,16 +503,16 @@ static async Task> GetCondidates(string package, NuGetVersion Cancel.None); return versions - .Where(_ => ShouldConsiderVersion(_, currentVersion, includePrerelease)) + .Where(_ => ShouldConsiderVersion(_, currentVersion)) .OrderDescending() .ToList(); } - static bool ShouldConsiderVersion(NuGetVersion candidate, NuGetVersion current, bool includePrerelease) + static bool ShouldConsiderVersion(NuGetVersion candidate, NuGetVersion current) { - // If current is stable, only consider stable or newer versions, unless pre-releases are explicitly allowed + // If current is stable, only consider stable or newer versions // If current is pre-release, consider any newer version - if (!includePrerelease && !current.IsPrerelease && candidate.IsPrerelease) + if (!current.IsPrerelease && candidate.IsPrerelease) { return false; } diff --git a/src/PackageUpdate/nuget.md b/src/PackageUpdate/nuget.md index 64b7f88..d067010 100644 --- a/src/PackageUpdate/nuget.md +++ b/src/PackageUpdate/nuget.md @@ -34,7 +34,7 @@ If no directory is passed the current directory will be used. * Updates all packages across all solutions in a directory * Respects `Pinned="true"` attribute to skip specific packages - * Updates `VersionOverride` entries (e.g. test projects tracking a pre-release) independently of the central stable version + * Updates `VersionOverride` entries in projects independently of the central version * Automatically migrates deprecated packages to recommended alternatives * Preserves file formatting (newlines, indentation) * Queries all configured NuGet sources diff --git a/src/Tests/UpdaterTests.cs b/src/Tests/UpdaterTests.cs index 608ea1d..1b9197b 100644 --- a/src/Tests/UpdaterTests.cs +++ b/src/Tests/UpdaterTests.cs @@ -1507,6 +1507,9 @@ public async Task UpdatesVersionOverrideInCsproj() await Assert.That(NuGetVersion.TryParse(updatedOverride, out var overrideVersion)).IsTrue(); await Assert.That(overrideVersion! > NuGetVersion.Parse("12.0.1")).IsTrue(); + // A stable override must stay stable, never jumping to a pre-release + await Assert.That(overrideVersion!.IsPrerelease).IsFalse(); + // The central stable version must stay stable var centralVersion = XDocument.Parse(props) .Descendants("PackageVersion") From 6e429c8e4887432ab965bf4427e553fc13520485 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 18 Jul 2026 21:52:17 +1000 Subject: [PATCH 3/3] . --- src/Directory.Build.props | 2 +- src/PackageUpdate/ProcessOutputReader.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e4b4426..29e8ef4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 4.2.0 + 4.3.0 preview NU1608 1.0.0 diff --git a/src/PackageUpdate/ProcessOutputReader.cs b/src/PackageUpdate/ProcessOutputReader.cs index 9759199..fb603b2 100644 --- a/src/PackageUpdate/ProcessOutputReader.cs +++ b/src/PackageUpdate/ProcessOutputReader.cs @@ -9,6 +9,7 @@ public static async Task> ReadLines(this Process process) { continue; } + list.Add(line); }