From 489d511c46ed99091803f126bf41ffee7763bb56 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 14 May 2026 15:02:37 -0700 Subject: [PATCH 1/2] Fix guest AppHost update with older CLI Preflight guest AppHost package updates when the target SDK is newer than the running CLI so users can update the CLI first. Defer saving aspire.config.json until guest SDK regeneration succeeds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Commands/UpdateCommand.cs | 74 +++++++ .../DotNetBasedAppHostServerProject.cs | 11 +- .../Projects/GuestAppHostProject.cs | 26 ++- .../Projects/IAppHostServerProject.cs | 4 +- .../Projects/PrebuiltAppHostServer.cs | 6 +- .../Commands/UpdateCommandTests.cs | 190 ++++++++++++++++++ .../Projects/AppHostServerSessionTests.cs | 3 +- .../Projects/GuestAppHostProjectTests.cs | 32 ++- .../TestServices/TestAppHostProjectFactory.cs | 16 +- 9 files changed, 320 insertions(+), 42 deletions(-) diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index 6290f31f994..5596eb131d1 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -15,6 +15,7 @@ using Aspire.Cli.Utils; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Semver; using Spectre.Console; namespace Aspire.Cli.Commands; @@ -242,6 +243,12 @@ protected override async Task ExecuteAsync(ParseResult parseResul ConfirmBinding = confirmBinding, NuGetConfigDirBinding = nugetConfigDirBinding }; + var cliUpdateResult = await TryUpdateCliBeforeGuestProjectUpdateAsync(project, projectFile, channel, confirmBinding, parseResult, cancellationToken); + if (cliUpdateResult is not null) + { + return cliUpdateResult; + } + await project.UpdatePackagesAsync(updateContext, cancellationToken); // After successful project update, check if CLI update is available and prompt @@ -312,6 +319,73 @@ protected override async Task ExecuteAsync(ParseResult parseResul return CommandResult.FromExitCode(0); } + private async Task TryUpdateCliBeforeGuestProjectUpdateAsync( + IAppHostProject project, + FileInfo projectFile, + PackageChannel channel, + PromptBinding confirmBinding, + ParseResult parseResult, + CancellationToken cancellationToken) + { + if (_cliDownloader is null || + string.IsNullOrEmpty(channel.CliDownloadBaseUrl) || + project.LanguageId.Equals(KnownLanguageId.CSharp, StringComparison.OrdinalIgnoreCase) || + projectFile.Directory is not { } projectDirectory) + { + return null; + } + + var targetSdkVersion = await GetLatestGuestSdkVersionAsync(channel, projectDirectory, cancellationToken); + if (targetSdkVersion is null || + !SemVersion.TryParse(VersionHelper.GetDefaultSdkVersion(), SemVersionStyles.Strict, out var currentCliVersion) || + SemVersion.PrecedenceComparer.Compare(targetSdkVersion, currentCliVersion) <= 0) + { + return null; + } + + var shouldUpdateCli = await InteractionService.PromptConfirmAsync( + UpdateCommandStrings.UpdateCliAfterProjectUpdatePrompt, + binding: confirmBinding, + cancellationToken: cancellationToken); + + if (!shouldUpdateCli) + { + return null; + } + + var dotNetToolUpdateCommand = GetDotNetToolUpdateCommand(); + if (dotNetToolUpdateCommand is not null) + { + InteractionService.DisplayMessage(KnownEmojis.Information, UpdateCommandStrings.DotNetToolSelfUpdateMessage); + InteractionService.DisplayPlainText($" {dotNetToolUpdateCommand}"); + return CommandResult.Success(); + } + + return await ExecuteSelfUpdateAsync(parseResult, cancellationToken, channel.Name); + } + + private async Task GetLatestGuestSdkVersionAsync(PackageChannel channel, DirectoryInfo projectDirectory, CancellationToken cancellationToken) + { + try + { + var sdkPackages = await channel.GetPackagesAsync("Aspire.Hosting", projectDirectory, cancellationToken); + return sdkPackages + .Select(static p => SemVersion.TryParse(p.Version, SemVersionStyles.Strict, out var version) ? version : null) + .OfType() + .OrderByDescending(static version => version, SemVersion.PrecedenceComparer) + .FirstOrDefault(); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to check target Aspire SDK version before project update"); + return null; + } + } + private async Task ExecuteSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken, string? selectedChannel = null) { var channel = selectedChannel ?? parseResult.GetValue(_channelOption) ?? parseResult.GetValue(_qualityOption); diff --git a/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs b/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs index 67540c8ddfa..bd3c6f46aa0 100644 --- a/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs +++ b/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs @@ -254,7 +254,8 @@ private XDocument CreateProjectFile(IEnumerable integratio /// public async Task<(string ProjectPath, string? ChannelName)> CreateProjectFilesAsync( IEnumerable integrations, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + string? requestedChannel = null) { // Clean obj folder to ensure fresh NuGet restore var objPath = Path.Combine(_projectModelPath, "obj"); @@ -321,7 +322,8 @@ private XDocument CreateProjectFile(IEnumerable integratio } var channels = await _packagingService.GetChannelsAsync(cancellationToken); - var configuredChannelName = AspireConfigFile.Load(_appPath)?.Channel + var configuredChannelName = requestedChannel + ?? AspireConfigFile.Load(_appPath)?.Channel ?? AspireJsonConfiguration.Load(_appPath)?.Channel; // Resolve channel sources and add them via RestoreAdditionalProjectSources @@ -417,9 +419,10 @@ private XDocument CreateProjectFile(IEnumerable integratio public async Task PrepareAsync( string sdkVersion, IEnumerable integrations, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + string? requestedChannel = null) { - var (_, channelName) = await CreateProjectFilesAsync(integrations, cancellationToken); + var (_, channelName) = await CreateProjectFilesAsync(integrations, cancellationToken, requestedChannel); var (buildSuccess, buildOutput) = await BuildAsync(cancellationToken); if (!buildSuccess) diff --git a/src/Aspire.Cli/Projects/GuestAppHostProject.cs b/src/Aspire.Cli/Projects/GuestAppHostProject.cs index aa4badf5931..f3b739da7c0 100644 --- a/src/Aspire.Cli/Projects/GuestAppHostProject.cs +++ b/src/Aspire.Cli/Projects/GuestAppHostProject.cs @@ -224,9 +224,10 @@ private string GetPrepareSdkVersion(AspireConfigFile config) IAppHostServerProject appHostServerProject, string sdkVersion, List integrations, + string? requestedChannel, CancellationToken cancellationToken) { - var result = await appHostServerProject.PrepareAsync(sdkVersion, integrations, cancellationToken); + var result = await appHostServerProject.PrepareAsync(sdkVersion, integrations, cancellationToken, requestedChannel); return (result.Success, result.Output, result.ChannelName, result.NeedsCodeGeneration); } @@ -235,15 +236,22 @@ private string GetPrepareSdkVersion(AspireConfigFile config) /// /// if the code was generated successfully; otherwise, . internal async Task BuildAndGenerateSdkAsync(DirectoryInfo directory, CancellationToken cancellationToken) + { + var config = LoadConfiguration(directory); + return await BuildAndGenerateSdkAsync(directory, config, cancellationToken); + } + + private async Task BuildAndGenerateSdkAsync(DirectoryInfo directory, AspireConfigFile config, CancellationToken cancellationToken) { var appHostServerProject = await _appHostServerProjectFactory.CreateAsync(directory.FullName, cancellationToken); - // Step 1: Load config - source of truth for SDK version and packages - var config = LoadConfiguration(directory); + // Step 1: Use the supplied config as the source of truth. Update uses an + // in-memory config here so a failed generation does not leave + // aspire.config.json pinned to versions the current CLI cannot run. var integrations = await GetIntegrationReferencesAsync(config, directory, cancellationToken); var sdkVersion = GetPrepareSdkVersion(config); - var (buildSuccess, buildOutput, _, _) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, integrations, cancellationToken); + var (buildSuccess, buildOutput, _, _) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, integrations, config.Channel, cancellationToken); if (!buildSuccess) { if (buildOutput is not null) @@ -355,7 +363,7 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken async () => { // Prepare the AppHost server (build for dev mode, restore for prebuilt) - var (prepareSuccess, prepareOutput, channelName, needsCodeGen) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, integrations, cancellationToken); + var (prepareSuccess, prepareOutput, channelName, needsCodeGen) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, integrations, config.Channel, cancellationToken); if (!prepareSuccess) { return (Success: false, Output: prepareOutput, Error: "Failed to prepare app host.", ChannelName: (string?)null, NeedsCodeGen: false); @@ -848,7 +856,7 @@ public async Task PublishAsync(PublishContext context, CancellationToken ca var sdkVersion = GetPrepareSdkVersion(config); // Prepare the AppHost server (build for dev mode, restore for prebuilt) - var (prepareSuccess, prepareOutput, _, needsCodeGen) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, integrations, cancellationToken); + var (prepareSuccess, prepareOutput, _, needsCodeGen) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, integrations, config.Channel, cancellationToken); if (!prepareSuccess) { // Set OutputCollector so PipelineCommandBase can display errors @@ -1237,15 +1245,13 @@ public async Task UpdatePackagesAsync(UpdatePackagesContex { config.AddOrUpdatePackage(packageId, newVersion); } - SaveConfiguration(config, directory); - // Rebuild and regenerate SDK code with updated packages _interactionService.DisplayEmptyLine(); var regenerateResult = await _interactionService.ShowStatusAsync( UpdateCommandStrings.RegeneratingSdkCode, async () => { - var regenerateSuccess = await BuildAndGenerateSdkAsync(directory, cancellationToken); + var regenerateSuccess = await BuildAndGenerateSdkAsync(directory, config, cancellationToken); if (!regenerateSuccess) { @@ -1260,6 +1266,8 @@ public async Task UpdatePackagesAsync(UpdatePackagesContex return regenerateResult; } + SaveConfiguration(config, directory); + _interactionService.DisplayMessage(KnownEmojis.Package, UpdateCommandStrings.RegeneratedSdkCode); _interactionService.DisplayEmptyLine(); diff --git a/src/Aspire.Cli/Projects/IAppHostServerProject.cs b/src/Aspire.Cli/Projects/IAppHostServerProject.cs index a5fc0730826..a5e81a871df 100644 --- a/src/Aspire.Cli/Projects/IAppHostServerProject.cs +++ b/src/Aspire.Cli/Projects/IAppHostServerProject.cs @@ -41,11 +41,13 @@ internal interface IAppHostServerProject /// The Aspire SDK version to use. /// The integration references (NuGet packages and/or project references) required by the app host. /// Cancellation token. + /// The package channel to use for this prepare operation, or to use the project configuration. /// The preparation result indicating success/failure and any output. Task PrepareAsync( string sdkVersion, IEnumerable integrations, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken = default, + string? requestedChannel = null); /// /// Runs the AppHost server process. diff --git a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs index 63378ca0ea8..10425008b4f 100644 --- a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs +++ b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs @@ -123,12 +123,12 @@ public string GetServerPath() public async Task PrepareAsync( string sdkVersion, IEnumerable integrations, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + string? requestedChannel = null) { var integrationList = integrations.ToList(); var packageRefs = integrationList.Where(r => r.IsPackageReference).ToList(); var projectRefs = integrationList.Where(r => r.IsProjectReference).ToList(); - string? requestedChannel = null; try { @@ -140,7 +140,7 @@ public async Task PrepareAsync( // Resolve the channel the project requests for restore (aspire.config.json#channel, // with a legacy .aspire/settings.json#channel fallback). This is independent of the // running CLI's identity hive (CliExecutionContext.IdentityChannel). - requestedChannel = ResolveRequestedChannel(); + requestedChannel ??= ResolveRequestedChannel(); if (projectRefs.Count > 0) { diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 931b409b417..94b24a38c11 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -292,6 +292,176 @@ public async Task UpdateCommand_WhenProjectUpdatedSuccessfully_AndChannelSupport Assert.Equal(ExitCodeConstants.Success, exitCode); } + [Fact] + public async Task UpdateCommand_GuestProject_WhenTargetSdkNewerThanCli_PromptsForCliUpdateBeforeProjectUpdateAndSkipsWhenAccepted() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + using var processPathScope = DotNetToolDetection.UseProcessPathForTesting("/home/test/.dotnet/tools/.store/aspire.cli/9.4.0/aspire.cli.linux-x64/9.4.0/tools/net10.0/linux-x64/aspire"); + var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"); + File.WriteAllText(appHostPath, "// test apphost"); + + var updateProjectInvoked = false; + string? confirmPrompt = null; + var interactionService = new TestInteractionService + { + ConfirmCallback = (prompt, _) => + { + confirmPrompt = prompt; + return true; + } + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.ProjectLocatorFactory = _ => new TestProjectLocator() + { + UseOrFindAppHostProjectFileAsyncCallback = (_, _, _) => Task.FromResult(new FileInfo(appHostPath)) + }; + + options.AppHostProjectFactory = _ => new TestAppHostProjectFactory + { + CanHandleCallback = _ => true, + LanguageId = "typescript/nodejs", + DisplayName = "TypeScript (Node.js)", + DetectionPatterns = ["apphost.ts"], + UpdatePackagesAsyncCallback = (_, _) => + { + updateProjectInvoked = true; + return Task.FromResult(new UpdatePackagesResult { UpdatesApplied = true }); + } + }; + + options.InteractionServiceFactory = _ => interactionService; + options.PackagingServiceFactory = _ => new TestPackagingService + { + GetChannelsAsyncCallback = _ => Task.FromResult>( + [CreatePackageChannelWithGuestSdkVersion("99.0.0", cliDownloadBaseUrl: "https://example.test/aspire")]) + }; + }); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("update --apphost apphost.ts"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + Assert.False(updateProjectInvoked); + Assert.NotNull(confirmPrompt); + Assert.Contains("An update is available for the Aspire CLI", confirmPrompt); + Assert.Contains(interactionService.DisplayedPlainText, text => text.Contains("dotnet tool update -g Aspire.Cli", StringComparison.Ordinal)); + } + + [Fact] + public async Task UpdateCommand_GuestProject_WhenTargetSdkNewerThanCliAndCliUpdateDeclined_ContinuesProjectUpdate() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"); + File.WriteAllText(appHostPath, "// test apphost"); + + var updateProjectInvoked = false; + var confirmCallbackInvoked = false; + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.ProjectLocatorFactory = _ => new TestProjectLocator() + { + UseOrFindAppHostProjectFileAsyncCallback = (_, _, _) => Task.FromResult(new FileInfo(appHostPath)) + }; + + options.AppHostProjectFactory = _ => new TestAppHostProjectFactory + { + CanHandleCallback = _ => true, + LanguageId = "typescript/nodejs", + DisplayName = "TypeScript (Node.js)", + DetectionPatterns = ["apphost.ts"], + UpdatePackagesAsyncCallback = (_, _) => + { + updateProjectInvoked = true; + return Task.FromResult(new UpdatePackagesResult { UpdatesApplied = true }); + } + }; + + options.InteractionServiceFactory = _ => new TestInteractionService + { + ConfirmCallback = (_, _) => + { + confirmCallbackInvoked = true; + return false; + } + }; + options.PackagingServiceFactory = _ => new TestPackagingService + { + GetChannelsAsyncCallback = _ => Task.FromResult>( + [CreatePackageChannelWithGuestSdkVersion("99.0.0", cliDownloadBaseUrl: "https://example.test/aspire")]) + }; + }); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("update --apphost apphost.ts"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + Assert.True(confirmCallbackInvoked); + Assert.True(updateProjectInvoked); + } + + [Fact] + public async Task UpdateCommand_GuestProject_WhenChannelCannotDownloadCli_DoesNotPromptBeforeProjectUpdate() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"); + File.WriteAllText(appHostPath, "// test apphost"); + + var updateProjectInvoked = false; + var confirmCallbackInvoked = false; + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.ProjectLocatorFactory = _ => new TestProjectLocator() + { + UseOrFindAppHostProjectFileAsyncCallback = (_, _, _) => Task.FromResult(new FileInfo(appHostPath)) + }; + + options.AppHostProjectFactory = _ => new TestAppHostProjectFactory + { + CanHandleCallback = _ => true, + LanguageId = "typescript/nodejs", + DisplayName = "TypeScript (Node.js)", + DetectionPatterns = ["apphost.ts"], + UpdatePackagesAsyncCallback = (_, _) => + { + updateProjectInvoked = true; + return Task.FromResult(new UpdatePackagesResult { UpdatesApplied = true }); + } + }; + + options.InteractionServiceFactory = _ => new TestInteractionService + { + ConfirmCallback = (_, _) => + { + confirmCallbackInvoked = true; + return false; + } + }; + options.PackagingServiceFactory = _ => new TestPackagingService + { + GetChannelsAsyncCallback = _ => Task.FromResult>( + [CreatePackageChannelWithGuestSdkVersion("99.0.0", cliDownloadBaseUrl: null)]) + }; + }); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("update --apphost apphost.ts"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + Assert.False(confirmCallbackInvoked); + Assert.True(updateProjectInvoked); + } + [Fact] public async Task UpdateCommand_WhenProjectUpdatedSuccessfullyAndRunningAsDotnetTool_DisplaysDotnetToolUpdateCommand() { @@ -1928,6 +2098,26 @@ public async Task UpdateCommand_SelfUpdate_DoesNotWriteChannelToGlobalConfigurat // to roll back any prior write. That rollback path is covered by the stable row of // UpdateCommand_SelfUpdate_DoesNotWriteChannelToGlobalConfiguration above (asserts both // DoesNotContain set + DoesNotContain delete) — no standalone test required here. + + private static PackageChannel CreatePackageChannelWithGuestSdkVersion(string sdkVersion, string? cliDownloadBaseUrl) + { + var fakeCache = new FakeNuGetPackageCache + { + GetPackagesAsyncCallback = (_, packageId, _, _, _, _, _) => + Task.FromResult>( + [ + new Aspire.Shared.NuGetPackageCli { Id = packageId, Version = sdkVersion, Source = "test" } + ]) + }; + + return PackageChannel.CreateExplicitChannel( + "stable", + PackageChannelQuality.Stable, + [new PackageMapping("Aspire*", "https://api.nuget.org/v3/index.json")], + fakeCache, + configureGlobalPackagesFolder: false, + cliDownloadBaseUrl: cliDownloadBaseUrl); + } } // Helper class to track DisplayCancellationMessage calls diff --git a/tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs b/tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs index de3e08ef937..e695d1a730c 100644 --- a/tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs @@ -49,7 +49,8 @@ private sealed class RecordingAppHostServerProject : IAppHostServerProject public Task PrepareAsync( string sdkVersion, IEnumerable integrations, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default, + string? requestedChannel = null) => throw new NotSupportedException(); public (string SocketPath, Process Process, OutputCollector OutputCollector) Run( diff --git a/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs b/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs index 635aa49ec0e..982a4b0fa3b 100644 --- a/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs @@ -545,25 +545,19 @@ public void CreateGuestEnvironmentVariables_AspireEnvironmentTakesPrecedenceOver } /// - /// Regression test for wf3: aspire update on a project with an Implicit channel - /// (i.e. the user has not pinned a channel via --channel, per-project - /// aspire.config.json#channel, or a prompt selection) must NOT silently pin the - /// running CLI's identity channel into aspire.config.json#channel. + /// Regression test for issue #17077: aspire update must not leave + /// aspire.config.json advanced to newer package versions when guest SDK + /// regeneration fails. /// /// /// The test drives through the - /// code path that detects updates and saves the config to disk, then expects the call - /// to throw from BuildAndGenerateSdkAsync (because - /// throws). The channel save - /// happens before that throw, so we can inspect the on-disk - /// aspire.config.json#channel to assert it was NOT changed. + /// code path that detects updates, then expects the call to throw from + /// BuildAndGenerateSdkAsync because + /// throws. The on-disk config should still contain the original versions. /// [Fact] - public async Task UpdatePackagesAsync_ImplicitChannel_DoesNotPinIdentityIntoConfig() + public async Task UpdatePackagesAsync_WhenRegenerationFails_DoesNotMutateConfig() { - // Seed aspire.config.json with no channel pinned, an SDK version that is behind, - // and a single package entry. The fake nuget cache will return a newer version so - // the early "nothing to update" return is not taken. var configPath = Path.Combine(_workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName); await File.WriteAllTextAsync(configPath, """ { @@ -588,7 +582,6 @@ await File.WriteAllTextAsync(configPath, """ var interactionService = new TestInteractionService { - // Confirm the "Perform updates?" prompt so the channel-write code path runs. ConfirmCallback = (_, _) => true }; @@ -604,16 +597,14 @@ await File.WriteAllTextAsync(configPath, """ NuGetConfigDirBinding = PromptBinding.CreateDefault(null), }; - // BuildAndGenerateSdkAsync calls IAppHostServerProjectFactory.CreateAsync, which the - // test factory throws from. The channel save happens BEFORE that, so the on-disk - // assertion below is still meaningful. await Assert.ThrowsAnyAsync( () => project.UpdatePackagesAsync(context, CancellationToken.None)); var reloaded = AspireConfigFile.Load(_workspace.WorkspaceRoot.FullName); Assert.NotNull(reloaded); - // Pre-fix, this would have been "pr-99999" (the identity channel). Post-fix, the - // implicit-channel update path leaves the channel untouched. + Assert.Equal("1.0.0", reloaded.SdkVersion); + Assert.NotNull(reloaded.Packages); + Assert.Equal("1.0.0", reloaded.Packages["Aspire.Hosting"]); Assert.Null(reloaded.Channel); } @@ -741,7 +732,8 @@ private sealed class FakeFailingAppHostServerProject(string appDirectoryPath) : public Task PrepareAsync( string sdkVersion, IEnumerable integrations, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default, + string? requestedChannel = null) => Task.FromResult(new AppHostServerPrepareResult(Success: false, Output: null)); public (string SocketPath, System.Diagnostics.Process Process, OutputCollector OutputCollector) Run( diff --git a/tests/Aspire.Cli.Tests/TestServices/TestAppHostProjectFactory.cs b/tests/Aspire.Cli.Tests/TestServices/TestAppHostProjectFactory.cs index a73404115dd..254ff338da2 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestAppHostProjectFactory.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestAppHostProjectFactory.cs @@ -34,6 +34,12 @@ internal sealed class TestAppHostProjectFactory : IAppHostProjectFactory /// public Func>? ValidateAppHostAsyncCallback { get; set; } + public Func>? UpdatePackagesAsyncCallback { get; set; } + + public string LanguageId { get; set; } = "csharp"; + + public string DisplayName { get; set; } = "C# (.NET)"; + /// /// Optional detection patterns to advertise from the test project. /// @@ -83,7 +89,7 @@ public IAppHostProject GetProject(FileInfo appHostFile) public IAppHostProject? GetProjectByLanguageId(string languageId) { - if (languageId.Equals("csharp", StringComparison.OrdinalIgnoreCase)) + if (languageId.Equals(LanguageId, StringComparison.OrdinalIgnoreCase)) { return _testProject; } @@ -140,8 +146,8 @@ public TestAppHostProject(TestAppHostProjectFactory factory) } public bool IsUnsupported { get; set; } - public string LanguageId => "csharp"; - public string DisplayName => "C# (.NET)"; + public string LanguageId => _factory.LanguageId; + public string DisplayName => _factory.DisplayName; public string? AppHostFileName => "AppHost.csproj"; public bool IsUsingProjectReferences(FileInfo appHostFile) @@ -220,7 +226,9 @@ public Task AddPackageAsync(AddPackageContext context, CancellationToken c => throw new NotImplementedException(); public Task UpdatePackagesAsync(UpdatePackagesContext context, CancellationToken cancellationToken) - => throw new NotImplementedException(); + => _factory.UpdatePackagesAsyncCallback is not null + ? _factory.UpdatePackagesAsyncCallback(context, cancellationToken) + : throw new NotImplementedException(); public Task FindAndStopRunningInstanceAsync(FileInfo appHostFile, DirectoryInfo homeDirectory, CancellationToken cancellationToken) => Task.FromResult(RunningInstanceResult.NoRunningInstance); From dc11ddd25efaa8bf0b30c389201e9752be3a2b86 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 14 May 2026 15:52:01 -0700 Subject: [PATCH 2/2] Address guest AppHost update review feedback Clarify the pre-update CLI prompt, explain that project update is skipped after accepting CLI update, and share guest AppHost SDK version resolution between the preflight and updater. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Commands/UpdateCommand.cs | 21 +++++++++------ src/Aspire.Cli/Packaging/PackageChannel.cs | 27 +++++++++++++++++++ .../Projects/GuestAppHostProject.cs | 6 +---- .../UpdateCommandStrings.Designer.cs | 2 ++ .../Resources/UpdateCommandStrings.resx | 6 +++++ .../Resources/xlf/UpdateCommandStrings.cs.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.de.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.es.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.fr.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.it.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.ja.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.ko.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.pl.xlf | 10 +++++++ .../xlf/UpdateCommandStrings.pt-BR.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.ru.xlf | 10 +++++++ .../Resources/xlf/UpdateCommandStrings.tr.xlf | 10 +++++++ .../xlf/UpdateCommandStrings.zh-Hans.xlf | 10 +++++++ .../xlf/UpdateCommandStrings.zh-Hant.xlf | 10 +++++++ .../Commands/UpdateCommandTests.cs | 4 ++- 19 files changed, 182 insertions(+), 14 deletions(-) diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index 5596eb131d1..b94f7949143 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -344,7 +344,7 @@ protected override async Task ExecuteAsync(ParseResult parseResul } var shouldUpdateCli = await InteractionService.PromptConfirmAsync( - UpdateCommandStrings.UpdateCliAfterProjectUpdatePrompt, + UpdateCommandStrings.UpdateCliBeforeGuestProjectUpdatePrompt, binding: confirmBinding, cancellationToken: cancellationToken); @@ -358,22 +358,27 @@ protected override async Task ExecuteAsync(ParseResult parseResul { InteractionService.DisplayMessage(KnownEmojis.Information, UpdateCommandStrings.DotNetToolSelfUpdateMessage); InteractionService.DisplayPlainText($" {dotNetToolUpdateCommand}"); + InteractionService.DisplayMessage(KnownEmojis.Information, UpdateCommandStrings.ProjectUpdateSkippedAfterCliUpdateMessage); return CommandResult.Success(); } - return await ExecuteSelfUpdateAsync(parseResult, cancellationToken, channel.Name); + var selfUpdateResult = await ExecuteSelfUpdateAsync(parseResult, cancellationToken, channel.Name); + if (selfUpdateResult.ExitCode == ExitCodeConstants.Success) + { + InteractionService.DisplayMessage(KnownEmojis.Information, UpdateCommandStrings.ProjectUpdateSkippedAfterCliUpdateMessage); + } + + return selfUpdateResult; } private async Task GetLatestGuestSdkVersionAsync(PackageChannel channel, DirectoryInfo projectDirectory, CancellationToken cancellationToken) { try { - var sdkPackages = await channel.GetPackagesAsync("Aspire.Hosting", projectDirectory, cancellationToken); - return sdkPackages - .Select(static p => SemVersion.TryParse(p.Version, SemVersionStyles.Strict, out var version) ? version : null) - .OfType() - .OrderByDescending(static version => version, SemVersion.PrecedenceComparer) - .FirstOrDefault(); + var sdkPackage = await channel.GetLatestGuestAppHostSdkPackageAsync(projectDirectory, cancellationToken); + return sdkPackage is not null && SemVersion.TryParse(sdkPackage.Version, SemVersionStyles.Strict, out var version) + ? version + : null; } catch (OperationCanceledException) { diff --git a/src/Aspire.Cli/Packaging/PackageChannel.cs b/src/Aspire.Cli/Packaging/PackageChannel.cs index e2e6e3b3320..0cce5207aa4 100644 --- a/src/Aspire.Cli/Packaging/PackageChannel.cs +++ b/src/Aspire.Cli/Packaging/PackageChannel.cs @@ -14,6 +14,8 @@ namespace Aspire.Cli.Packaging; internal class PackageChannel(string name, PackageChannelQuality quality, PackageMapping[]? mappings, INuGetPackageCache nuGetPackageCache, bool configureGlobalPackagesFolder = false, string? cliDownloadBaseUrl = null, string? pinnedVersion = null, ILogger? logger = null) { + private const string GuestAppHostSdkPackageId = "Aspire.Hosting"; + public string Name { get; } = name; public PackageChannelQuality Quality { get; } = quality; public PackageMapping[]? Mappings { get; } = mappings; @@ -219,6 +221,31 @@ public async Task> GetPackagesAsync(string packageId, return filteredPackages; } + public async Task GetLatestGuestAppHostSdkPackageAsync(DirectoryInfo workingDirectory, CancellationToken cancellationToken) + { + // Guest AppHost sdk.version resolves to the base Aspire.Hosting package because + // the managed server restores that package to evaluate and generate the AppHost. + var packages = await GetPackagesAsync(GuestAppHostSdkPackageId, workingDirectory, cancellationToken); + + NuGetPackage? latestPackage = null; + SemVersion? latestVersion = null; + foreach (var package in packages) + { + if (!SemVersion.TryParse(package.Version, SemVersionStyles.Strict, out var version)) + { + continue; + } + + if (latestVersion is null || SemVersion.PrecedenceComparer.Compare(version, latestVersion) > 0) + { + latestPackage = package; + latestVersion = version; + } + } + + return latestPackage; + } + public async Task> GetPackageVersionsAsync(string packageId, DirectoryInfo workingDirectory, CancellationToken cancellationToken) { var tasks = new List>>(); diff --git a/src/Aspire.Cli/Projects/GuestAppHostProject.cs b/src/Aspire.Cli/Projects/GuestAppHostProject.cs index f3b739da7c0..3d89a580cdf 100644 --- a/src/Aspire.Cli/Projects/GuestAppHostProject.cs +++ b/src/Aspire.Cli/Projects/GuestAppHostProject.cs @@ -1156,11 +1156,7 @@ public async Task UpdatePackagesAsync(UpdatePackagesContex // Check for SDK version update (silently - it's an implementation detail) try { - var sdkPackages = await context.Channel.GetPackagesAsync("Aspire.Hosting", directory, cancellationToken); - var latestSdkPackage = sdkPackages - .Where(p => SemVersion.TryParse(p.Version, SemVersionStyles.Strict, out _)) - .OrderByDescending(p => SemVersion.Parse(p.Version, SemVersionStyles.Strict), SemVersion.PrecedenceComparer) - .FirstOrDefault(); + var latestSdkPackage = await context.Channel.GetLatestGuestAppHostSdkPackageAsync(directory, cancellationToken); if (latestSdkPackage is not null && latestSdkPackage.Version != config.SdkVersion) { diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs index 4d11b077e99..0132b01a077 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs @@ -102,11 +102,13 @@ internal static string ProjectArgumentDescription { internal static string FallbackParsingWarning => ResourceManager.GetString("FallbackParsingWarning", resourceCulture); internal static string NoAppHostFoundUpdateCliPrompt => ResourceManager.GetString("NoAppHostFoundUpdateCliPrompt", resourceCulture); internal static string UpdateCliAfterProjectUpdatePrompt => ResourceManager.GetString("UpdateCliAfterProjectUpdatePrompt", resourceCulture); + internal static string UpdateCliBeforeGuestProjectUpdatePrompt => ResourceManager.GetString("UpdateCliBeforeGuestProjectUpdatePrompt", resourceCulture); internal static string ChannelOptionDescription => ResourceManager.GetString("ChannelOptionDescription", resourceCulture); internal static string ChannelOptionDescriptionWithStaging => ResourceManager.GetString("ChannelOptionDescriptionWithStaging", resourceCulture); internal static string QualityOptionDescription => ResourceManager.GetString("QualityOptionDescription", resourceCulture); internal static string QualityOptionDescriptionWithStaging => ResourceManager.GetString("QualityOptionDescriptionWithStaging", resourceCulture); internal static string DotNetToolSelfUpdateMessage => ResourceManager.GetString("DotNetToolSelfUpdateMessage", resourceCulture); + internal static string ProjectUpdateSkippedAfterCliUpdateMessage => ResourceManager.GetString("ProjectUpdateSkippedAfterCliUpdateMessage", resourceCulture); internal static string MigratedToNewSdkFormat => ResourceManager.GetString("MigratedToNewSdkFormat", resourceCulture); internal static string RemovedObsoleteAppHostPackage => ResourceManager.GetString("RemovedObsoleteAppHostPackage", resourceCulture); internal static string NoWritePermissionToInstallDirectory => ResourceManager.GetString("NoWritePermissionToInstallDirectory", resourceCulture); diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx index de575d67b7a..deb943bbd98 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx @@ -123,6 +123,9 @@ An update is available for the Aspire CLI. Would you like to update it now? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + Channel to update to (stable, daily) @@ -138,6 +141,9 @@ To update the Aspire CLI when installed as a .NET tool, run: + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Migrated to new project format: <Project Sdk="Aspire.AppHost.Sdk/{0}"> diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf index aa20cc66077..74e9c447d50 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf @@ -197,6 +197,11 @@ Projekt je aktuální! (nejsou potřeba žádné aktualizace) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Úroveň kvality, na kterou se má aktualizovat (stabilní, denní) @@ -252,6 +257,11 @@ Pro Aspire CLI je k dispozici aktualizace. Chcete aktualizaci provést nyní? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} Aktualizovat balíček {0} z {1} na {2} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf index 1034647812b..78e491f7ee3 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf @@ -197,6 +197,11 @@ Das Projekt ist auf dem neuesten Stand. (keine Updates erforderlich) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Qualitätsstufe, auf die aktualisiert werden soll (stable, daily) @@ -252,6 +257,11 @@ Für die Aspire-CLI ist ein Update verfügbar. Möchten Sie es jetzt installieren? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} Paket {0} von {1} auf {2} aktualisieren diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf index 817354bde05..66205fb2f91 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf @@ -197,6 +197,11 @@ ¡El proyecto está al día! (no es necesario actualizar) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Nivel de calidad a actualizar a (estable, diario) @@ -252,6 +257,11 @@ Hay una actualización disponible para la CLI de Aspire. ¿Quieres actualizarla ahora? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} Actualizar paquete {0} de {1} a {2} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf index bb81d4b058b..9b6fd1edd2b 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf @@ -197,6 +197,11 @@ Le projet est à jour ! (aucune mise à jour nécessaire) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Niveau de qualité vers lequel effectuer une mise à jour (stable, quotidien) @@ -252,6 +257,11 @@ Une mise à jour est disponible pour l’interface CLI Aspire. Voulez-vous la mettre à jour maintenant ? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} Mettre à jour le package {0} de {1} à {2} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf index 2e39559bdbc..5c8b852327e 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf @@ -197,6 +197,11 @@ Il progetto è aggiornato. (non sono necessari aggiornamenti) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Livello di qualità da aggiornare a (stabile, giornaliero) @@ -252,6 +257,11 @@ È disponibile un aggiornamento per l'interfaccia della riga di comando Aspire. Aggiornarla ora? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} Aggiorna il pacchetto {0} da {1} a {2} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf index 58f0d042195..19765dc212a 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf @@ -197,6 +197,11 @@ プロジェクトは最新の状態です!(更新は必要ありません) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) 更新先の品質レベル (安定、毎日) @@ -252,6 +257,11 @@ Aspire CLI の更新プログラムが利用可能です。今すぐ更新しますか? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} パッケージ {0} を {1} から {2} に更新する diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf index bdf0ba6229a..982ffaa7479 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf @@ -197,6 +197,11 @@ 프로젝트가 최신 상태입니다! (업데이트 필요 없음) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) 업데이트할 품질 수준(안정, 데일리) @@ -252,6 +257,11 @@ Aspire CLI 업데이트가 있습니다. 지금 업데이트하시겠어요? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} 패키지 {0}을(를) {1}에서 {2}(으)로 업데이트 diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf index 249238b4f53..09ec1f3494b 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf @@ -197,6 +197,11 @@ Projekt jest aktualny. (nie są wymagane żadne aktualizacje) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Poziom jakości, do którego należy zaktualizować (stabilny, dzienny) @@ -252,6 +257,11 @@ Dostępna jest aktualizacja narzędzia Aspire CLI. Czy chcesz teraz zaktualizować? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} Aktualizacja {0} z {1} do {2} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf index 950b43eb1d8..c72f4effdc9 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf @@ -197,6 +197,11 @@ O Project foi atualizado! (nenhuma atualização necessária) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Nível de qualidade para o qual atualizar (estável, diariamente) @@ -252,6 +257,11 @@ Há uma atualização disponível para a CLI do Aspire. Você quer atualizá-la agora? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} Atualizar o pacote {0} de {1} para {2} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf index 43bd862ec89..512a69e3c7b 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf @@ -197,6 +197,11 @@ Проект обновлен! (обновления не требуются) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Уровень качества для обновления (стабильный, ежедневно) @@ -252,6 +257,11 @@ Доступно обновление для Aspire CLI. Хотите обновить его сейчас? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} Обновить пакет {0} с{1} до {2} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf index 91964f63232..18e1d16c58d 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf @@ -197,6 +197,11 @@ Proje güncel! (güncelleştirme gerekmiyor) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) Güncelleştirilecek kalite seviyesi (kararlı, günlük) @@ -252,6 +257,11 @@ Aspire CLI için bir güncelleştirme var. Şimdi güncelleştirmek istiyor musunuz? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} {1} olan {0} paketini {2} olarak güncelleyin diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf index 408aede0fe6..597d10ab37c 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf @@ -197,6 +197,11 @@ 项目已是最新!(无需更新) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) 要更新到的质量级别(稳定、每日) @@ -252,6 +257,11 @@ Aspire CLI 有可用更新。是否现在更新? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} 将包 {0} 从 {1} 更新为 {2} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf index 2c09fa9134d..09e19b01b04 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf @@ -197,6 +197,11 @@ 專案為最新!(不需要更新) + + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + Project update skipped. Update the Aspire CLI, then re-run `aspire update`. + + Quality level to update to (stable, daily) 要更新的品質等級 (穩定、每日) @@ -252,6 +257,11 @@ 更新可用於 Aspire CLI。您現在要更新嗎? + + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + The selected Aspire SDK is newer than this Aspire CLI. Update the Aspire CLI now and re-run `aspire update` to update this project? + + Update package {0} from {1} to {2} 將套件 {0} 從 {1} 更新到 {2} diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 94b24a38c11..84c2d7f4611 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -348,8 +348,10 @@ public async Task UpdateCommand_GuestProject_WhenTargetSdkNewerThanCli_PromptsFo Assert.Equal(ExitCodeConstants.Success, exitCode); Assert.False(updateProjectInvoked); Assert.NotNull(confirmPrompt); - Assert.Contains("An update is available for the Aspire CLI", confirmPrompt); + Assert.Contains("newer than this Aspire CLI", confirmPrompt); + Assert.Contains("re-run `aspire update`", confirmPrompt); Assert.Contains(interactionService.DisplayedPlainText, text => text.Contains("dotnet tool update -g Aspire.Cli", StringComparison.Ordinal)); + Assert.Contains(interactionService.DisplayedMessages, message => message.Message.Contains("Project update skipped", StringComparison.Ordinal)); } [Fact]