From 31123a1ef58c1a748a250045d148a395a20e8aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zeynep=20Gizem=20F=C4=B1rat?= Date: Fri, 28 Aug 2026 15:24:07 +0300 Subject: [PATCH 1/2] Add CPM include support to switch-to-nightly Extend `switch-to-nightly` with `--include` and `--exclude-packages` options to optionally update Central Package Management files (like `Directory.Packages.props`) alongside regular project/package updates. The updater now resolves the nightly ABP version once, updates `PackageVersion Include="Volo.*"` entries with exclusion and LeptonX/Studio safeguards, and logs/continues on MyGet or file-write failures. Also fixes `.npmrc` registry appending to insert a newline before the added registry entry. --- .../Cli/Commands/SwitchToNightlyCommand.cs | 2 + .../ProjectModification/NpmPackagesUpdater.cs | 2 +- .../PackagePreviewSwitcher.cs | 95 ++++++++++++++-- .../VoloNugetPackagesVersionUpdater.cs | 103 +++++++++++++++++- 4 files changed, 191 insertions(+), 11 deletions(-) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs index b7dca40bad..3f1651eb77 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs @@ -32,6 +32,8 @@ public class SwitchToNightlyCommand : IConsoleCommand, ITransientDependency sb.AppendLine(""); sb.AppendLine("Options:"); sb.AppendLine("-d|--directory"); + sb.AppendLine("-i|--include (optional) comma-separated list of Directory.Packages.props-style files to also update for Central Package Management"); + sb.AppendLine("-ep|--exclude-packages (optional) comma-separated list of package ids to never touch in --include files"); sb.AppendLine(""); sb.AppendLine("See the documentation for more info: https://abp.io/docs/latest/cli"); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs index 9b7340fdd7..379fc45bfa 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs @@ -132,7 +132,7 @@ public class NpmPackagesUpdater : ITransientDependency if (!fileContent.Contains(volosoftRegistry)) { - fileContent += volosoftRegistry; + fileContent += Environment.NewLine + volosoftRegistry; } File.WriteAllText(fileName, fileContent); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs index 13046dae78..6addbd8bb8 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -66,13 +67,13 @@ public class PackagePreviewSwitcher : ITransientDependency if (solutionPaths.Any()) { - await SwitchSolutionsToNightlyPreview(solutionPaths); + await SwitchSolutionsToNightlyPreview(solutionPaths, commandLineArgs); } else { var projectPaths = GetProjectPaths(commandLineArgs); - - await SwitchProjectsToNightlyPreview(projectPaths); + + await SwitchProjectsToNightlyPreview(projectPaths, commandLineArgs); } } @@ -185,13 +186,16 @@ public class PackagePreviewSwitcher : ITransientDependency } } - private async Task SwitchProjectsToNightlyPreview(List projects) + private async Task SwitchProjectsToNightlyPreview(List projects, CommandLineArgs commandLineArgs) { + var (includeFiles, excludedPackages, latestVersionFromMyGet) = await ResolveNightlyIncludeContextAsync(commandLineArgs); + foreach (var project in projects) { var folder = Path.GetDirectoryName(project); + var projectFolder = FindSolutionFolder(project) ?? folder; - _packageSourceManager.Add(FindSolutionFolder(project) ?? folder, "ABP Nightly", + _packageSourceManager.Add(projectFolder, "ABP Nightly", "https://www.myget.org/F/abp-nightly/api/v3/index.json", "Volo.*"); await _nugetPackagesVersionUpdater.UpdateSolutionAsync( @@ -201,11 +205,17 @@ public class PackagePreviewSwitcher : ITransientDependency await _npmPackagesUpdater.Update( folder, true); + + // See SwitchSolutionsToNightlyPreview for the race-avoidance rationale: this + // sequential pass always runs after the per-project UpdateSolutionAsync above. + await UpdateIncludedCentralPackageFilesAsync(includeFiles, excludedPackages, latestVersionFromMyGet, projectFolder); } } - private async Task SwitchSolutionsToNightlyPreview(List solutionPaths) + private async Task SwitchSolutionsToNightlyPreview(List solutionPaths, CommandLineArgs commandLineArgs) { + var (includeFiles, excludedPackages, latestVersionFromMyGet) = await ResolveNightlyIncludeContextAsync(commandLineArgs); + foreach (var solutionPath in solutionPaths) { var solutionFolder = Path.GetDirectoryName(solutionPath); @@ -232,6 +242,59 @@ public class PackagePreviewSwitcher : ITransientDependency solutionAngularFolder, true); } + + // Optional Central Package Management support: only runs when --include is + // explicitly passed, and only after UpdateSolutionAsync's internal parallel + // (Task.WaitAll) per-project update has fully completed, so no --include file + // is ever touched concurrently with anything else. + await UpdateIncludedCentralPackageFilesAsync(includeFiles, excludedPackages, latestVersionFromMyGet, solutionFolder); + } + } + + private async Task<(List IncludeFiles, List ExcludedPackages, string LatestVersionFromMyGet)> ResolveNightlyIncludeContextAsync( + CommandLineArgs commandLineArgs) + { + var includeFiles = GetCommaSeparatedOption(commandLineArgs, Options.Include.Short, Options.Include.Long); + var excludedPackages = GetCommaSeparatedOption(commandLineArgs, Options.Exclude.Short, Options.Exclude.Long); + + if (!includeFiles.Any()) + { + return (includeFiles, excludedPackages, null); + } + + string latestVersionFromMyGet; + try + { + latestVersionFromMyGet = await _nugetPackagesVersionUpdater.GetLatestVersionFromMyGet("Volo.Abp.Core"); + } + catch (Exception ex) + { + // Don't let a transient MyGet failure abort the whole switch-to-nightly run + // (source registration / regular PackageReference updates below must still + // proceed for every solution/project) - just skip the --include pass. + Logger.LogWarning(ex, "Could not resolve the latest Volo.Abp.Core nightly version; --include files will be skipped for this run."); + latestVersionFromMyGet = null; + } + + return (includeFiles, excludedPackages, latestVersionFromMyGet); + } + + private async Task UpdateIncludedCentralPackageFilesAsync( + List includeFiles, + List excludedPackages, + string latestVersionFromMyGet, + string baseFolder) + { + foreach (var includeFile in includeFiles) + { + var resolvedPath = Path.IsPathRooted(includeFile) + ? includeFile + : Path.Combine(baseFolder, includeFile); + + await _nugetPackagesVersionUpdater.UpdateCentralPackageVersionsAsync( + resolvedPath, + latestVersionFromMyGet, + excludedPackages); } } @@ -285,6 +348,14 @@ public class PackagePreviewSwitcher : ITransientDependency ?? Directory.GetCurrentDirectory(); } + private List GetCommaSeparatedOption(CommandLineArgs commandLineArgs, string shortName, string longName) + { + var raw = commandLineArgs.Options.GetOrNull(shortName, longName); + return raw.IsNullOrWhiteSpace() + ? new List() + : raw.Split(',').Select(s => s.Trim()).Where(s => !s.IsNullOrWhiteSpace()).ToList(); + } + private string GetSolutionAngularFolder(string solutionFolder) { var upperAngularPath = Path.Combine(Directory.GetParent(solutionFolder)?.FullName ?? "", "angular"); @@ -340,5 +411,15 @@ public class PackagePreviewSwitcher : ITransientDependency public const string Short = "d"; public const string Long = "directory"; } + public static class Include + { + public const string Short = "i"; + public const string Long = "include"; + } + public static class Exclude + { + public const string Short = "ep"; + public const string Long = "exclude-packages"; + } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs index 924d6041b1..6500674799 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs @@ -217,8 +217,8 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency } var currentVersion = versionAttribute.Value; - var isLeptonXPackage = packageId.Contains("LeptonX"); - var isStudioPackage = packageId.StartsWith("Volo.Abp.Studio."); + var isLeptonXPackage = IsLeptonXPackage(packageId); + var isStudioPackage = IsStudioPackage(packageId); if(isLeptonXPackage) { //'SemanticVersion.TryParse' can not parse the version if the version contains floating version resolution, such as '*-*' @@ -366,10 +366,107 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency return await Task.FromResult(content); } - private async Task GetLatestVersionFromMyGet(string packageId) + private static bool IsLeptonXPackage(string packageId) => packageId.Contains("LeptonX"); + + private static bool IsStudioPackage(string packageId) => packageId.StartsWith("Volo.Abp.Studio."); + + internal async Task GetLatestVersionFromMyGet(string packageId) { var myGetPack = await _myGetPackageListFinder.GetPackagesAsync(); return myGetPack.Packages.FirstOrDefault(p => p.Id == packageId)?.Versions.LastOrDefault(); } + + /// + /// Updates <PackageVersion Include="Volo.*"> entries in a Central Package Management + /// props file (e.g. Directory.Packages.props) to . + /// Regular PackageReference-based updates (UpdateSolutionAsync/UpdateProjectAsync) already + /// skip any PackageReference with no Version attribute (i.e. CPM-managed packages) - this + /// method is the explicit, opt-in counterpart for callers that also want those central + /// versions kept in sync. Not invoked unless a caller (e.g. the switch-to-nightly --include + /// option) explicitly requests it. + /// + public async Task UpdateCentralPackageVersionsAsync( + string filePath, + string latestVersionFromMyGet, + IEnumerable excludedPackageIds = null) + { + if (!File.Exists(filePath)) + { + Logger.LogWarning("--include file not found, skipped: {FilePath}", filePath); + return; + } + + if (latestVersionFromMyGet == null) + { + return; + } + + var excluded = new HashSet(excludedPackageIds ?? Enumerable.Empty(), StringComparer.OrdinalIgnoreCase); + + try + { + using (var fs = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + { + using (var sr = new StreamReader(fs, Encoding.Default, true)) + { + var fileContent = await sr.ReadToEndAsync(); + + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(fileContent); + + var packageNodeList = doc.SelectNodes("//PackageVersion[starts-with(@Include, 'Volo.')]"); + if (packageNodeList != null) + { + foreach (XmlNode package in packageNodeList) + { + var packageId = package.Attributes?["Include"]?.Value; + if (packageId == null || excluded.Contains(packageId)) + { + continue; + } + + // LeptonX and Studio packages follow their own, independent version + // stream (see IsLeptonXPackage/IsStudioPackage, also used by + // UpdateVoloPackagesAsync above) - never stamp them with the + // Volo.Abp.Core anchor version, regardless of --exclude-packages. + if (IsLeptonXPackage(packageId) || IsStudioPackage(packageId)) + { + continue; + } + + var versionAttribute = package.Attributes["Version"]; + if (versionAttribute == null) + { + continue; + } + + if (versionAttribute.Value != latestVersionFromMyGet) + { + Logger.LogInformation("Updating central package \"{PackageId}\" from v{CurrentVersion} to v{LatestVersion}", packageId, versionAttribute.Value, latestVersionFromMyGet); + versionAttribute.Value = latestVersionFromMyGet; + } + } + } + + fs.Seek(0, SeekOrigin.Begin); + fs.SetLength(0); + + using (var sw = new StreamWriter(fs, DefaultEncoding)) + { + await sw.WriteAsync(doc.OuterXml); + await sw.FlushAsync(); + } + } + } + } + catch (Exception ex) + { + // The file is truncated before the updated XML is written back, so a failure here + // (disk full, process killed, file locked mid-write) can leave it empty/partially + // written on disk. Logged as an error (not a warning) so this isn't missed - the + // rest of the switch-to-nightly run still continues for other solutions/files. + Logger.LogError(ex, "Failed to update central package versions in \"{FilePath}\". The file may now be empty or partially written - please check it manually.", filePath); + } + } } From c217d58f2e4240e5a86e1a14683c58195e1caea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zeynep=20Gizem=20F=C4=B1rat?= Date: Mon, 31 Aug 2026 10:55:46 +0300 Subject: [PATCH 2/2] Handle missing nightly version for --include Improve CLI package update behavior when resolving the latest `Volo.Abp.Core` nightly version fails or returns empty: the `--include` pass is now skipped explicitly with a warning while normal project updates continue. Also switch file reading in `VoloNugetPackagesVersionUpdater` to use the shared `DefaultEncoding` instead of `Encoding.Default` for consistent encoding handling. --- .../Cli/ProjectModification/PackagePreviewSwitcher.cs | 10 +++++++++- .../VoloNugetPackagesVersionUpdater.cs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs index 6addbd8bb8..9dfacf5431 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs @@ -273,7 +273,15 @@ public class PackagePreviewSwitcher : ITransientDependency // (source registration / regular PackageReference updates below must still // proceed for every solution/project) - just skip the --include pass. Logger.LogWarning(ex, "Could not resolve the latest Volo.Abp.Core nightly version; --include files will be skipped for this run."); - latestVersionFromMyGet = null; + return (includeFiles, excludedPackages, null); + } + + if (latestVersionFromMyGet.IsNullOrWhiteSpace()) + { + // No exception was thrown, but MyGet simply has no version for this package yet + // (e.g. not indexed there) - warn so users aren't left wondering why --include did nothing. + Logger.LogWarning("Could not resolve the latest Volo.Abp.Core nightly version; --include files will be skipped for this run."); + return (includeFiles, excludedPackages, null); } return (includeFiles, excludedPackages, latestVersionFromMyGet); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs index 6500674799..f9aedb29b9 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs @@ -408,7 +408,7 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency { using (var fs = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { - using (var sr = new StreamReader(fs, Encoding.Default, true)) + using (var sr = new StreamReader(fs, DefaultEncoding, true)) { var fileContent = await sr.ReadToEndAsync();