Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/Aspire.Cli/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -242,6 +243,12 @@ protected override async Task<CommandResult> 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
Expand Down Expand Up @@ -312,6 +319,73 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul
return CommandResult.FromExitCode(0);
}

private async Task<CommandResult?> TryUpdateCliBeforeGuestProjectUpdateAsync(
IAppHostProject project,
FileInfo projectFile,
PackageChannel channel,
PromptBinding<bool> 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,
Comment thread
sebastienros marked this conversation as resolved.
Outdated
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}");
Comment thread
sebastienros marked this conversation as resolved.
return CommandResult.Success();
}

return await ExecuteSelfUpdateAsync(parseResult, cancellationToken, channel.Name);
}

private async Task<SemVersion?> 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<SemVersion>()
.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;
}
}
Comment thread
sebastienros marked this conversation as resolved.

private async Task<CommandResult> ExecuteSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken, string? selectedChannel = null)
{
var channel = selectedChannel ?? parseResult.GetValue(_channelOption) ?? parseResult.GetValue(_qualityOption);
Expand Down
11 changes: 7 additions & 4 deletions src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ private XDocument CreateProjectFile(IEnumerable<IntegrationReference> integratio
/// </summary>
public async Task<(string ProjectPath, string? ChannelName)> CreateProjectFilesAsync(
IEnumerable<IntegrationReference> integrations,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default,
string? requestedChannel = null)
{
// Clean obj folder to ensure fresh NuGet restore
var objPath = Path.Combine(_projectModelPath, "obj");
Expand Down Expand Up @@ -321,7 +322,8 @@ private XDocument CreateProjectFile(IEnumerable<IntegrationReference> 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
Expand Down Expand Up @@ -417,9 +419,10 @@ private XDocument CreateProjectFile(IEnumerable<IntegrationReference> integratio
public async Task<AppHostServerPrepareResult> PrepareAsync(
string sdkVersion,
IEnumerable<IntegrationReference> 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)
Expand Down
26 changes: 17 additions & 9 deletions src/Aspire.Cli/Projects/GuestAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,10 @@ private string GetPrepareSdkVersion(AspireConfigFile config)
IAppHostServerProject appHostServerProject,
string sdkVersion,
List<IntegrationReference> 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);
}

Expand All @@ -235,15 +236,22 @@ private string GetPrepareSdkVersion(AspireConfigFile config)
/// </summary>
/// <returns><see langword="true"/> if the code was generated successfully; otherwise, <see langword="false"/>.</returns>
internal async Task<bool> BuildAndGenerateSdkAsync(DirectoryInfo directory, CancellationToken cancellationToken)
{
var config = LoadConfiguration(directory);
return await BuildAndGenerateSdkAsync(directory, config, cancellationToken);
}

private async Task<bool> 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)
Expand Down Expand Up @@ -355,7 +363,7 @@ public async Task<int> 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);
Expand Down Expand Up @@ -848,7 +856,7 @@ public async Task<int> 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
Expand Down Expand Up @@ -1237,15 +1245,13 @@ public async Task<UpdatePackagesResult> 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)
{
Expand All @@ -1260,6 +1266,8 @@ public async Task<UpdatePackagesResult> UpdatePackagesAsync(UpdatePackagesContex
return regenerateResult;
}

SaveConfiguration(config, directory);

_interactionService.DisplayMessage(KnownEmojis.Package, UpdateCommandStrings.RegeneratedSdkCode);

_interactionService.DisplayEmptyLine();
Expand Down
4 changes: 3 additions & 1 deletion src/Aspire.Cli/Projects/IAppHostServerProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ internal interface IAppHostServerProject
/// <param name="sdkVersion">The Aspire SDK version to use.</param>
/// <param name="integrations">The integration references (NuGet packages and/or project references) required by the app host.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="requestedChannel">The package channel to use for this prepare operation, or <see langword="null" /> to use the project configuration.</param>
/// <returns>The preparation result indicating success/failure and any output.</returns>
Task<AppHostServerPrepareResult> PrepareAsync(
string sdkVersion,
IEnumerable<IntegrationReference> integrations,
CancellationToken cancellationToken = default);
CancellationToken cancellationToken = default,
string? requestedChannel = null);

/// <summary>
/// Runs the AppHost server process.
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ public string GetServerPath()
public async Task<AppHostServerPrepareResult> PrepareAsync(
string sdkVersion,
IEnumerable<IntegrationReference> 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
{
Expand All @@ -140,7 +140,7 @@ public async Task<AppHostServerPrepareResult> 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)
{
Expand Down
Loading
Loading