From 893eec63b19171fa9219570ee878a9ebd7a77823 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 4 Apr 2025 07:03:59 +0000 Subject: [PATCH 1/6] Make publishers prompt async for faster cancellation. --- src/Aspire.Cli/Commands/PublishCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index df9c19d8d97..5367d99776d 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -125,7 +125,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell .HighlightStyle(Style.Parse("darkmagenta")) .AddChoices(publishers!); - publisher = AnsiConsole.Prompt(publisherPrompt); + publisher = await AnsiConsole.PromptAsync(publisherPrompt, cancellationToken); } AnsiConsole.MarkupLine($":hammer_and_wrench: Generating artifacts for '{publisher}' publisher..."); From bf68dc2e234dc825fe27763fb760176600e58589 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 4 Apr 2025 07:34:54 +0000 Subject: [PATCH 2/6] Make AppHost crash if no backchannel is attached and publishing fails. --- src/Aspire.Hosting/DistributedApplicationRunner.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Aspire.Hosting/DistributedApplicationRunner.cs b/src/Aspire.Hosting/DistributedApplicationRunner.cs index d953dfd561e..8af3ce11a76 100644 --- a/src/Aspire.Hosting/DistributedApplicationRunner.cs +++ b/src/Aspire.Hosting/DistributedApplicationRunner.cs @@ -67,6 +67,11 @@ await eventing.PublishAsync( logger.LogError(ex, "Failed to publish the distributed application."); publishingActivity.IsError = true; await activityReporter.UpdateActivityAsync(publishingActivity, stoppingToken).ConfigureAwait(false); + + if (!backchannelService.IsBackchannelExpected) + { + lifetime.StopApplication(); + } } } } From 854e1e66e04041f27f2de17ae4d6818eae95f6c5 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 4 Apr 2025 07:48:48 +0000 Subject: [PATCH 3/6] Switch LogError to LogDebug (whoops) --- src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs index 665b3e91847..cd463079792 100644 --- a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs +++ b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs @@ -140,7 +140,7 @@ private async Task BuildProjectContainerImageAsync(IResource resource, C publishingActivity.IsComplete = true; await activityReporter.UpdateActivityAsync(publishingActivity, cancellationToken).ConfigureAwait(false); - logger.LogError( + logger.LogDebug( ".NET CLI completed with exit code: {ExitCode}", process.ExitCode); From 225c07e73455a861952f06edf68abad59b922e24 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 4 Apr 2025 08:01:54 +0000 Subject: [PATCH 4/6] Return correct result from TryGetContainerImageName in compose publisher. Add Dockerfile resource to playground for verification. --- .../publishers/Publishers.AppHost/Program.cs | 2 ++ .../Publishers.AppHost/qots/Dockerfile | 12 ++++++++ .../Publishers.AppHost/qots/qots.go | 28 +++++++++++++++++++ .../DockerComposePublishingContext.cs | 2 +- 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 playground/publishers/Publishers.AppHost/qots/Dockerfile create mode 100644 playground/publishers/Publishers.AppHost/qots/qots.go diff --git a/playground/publishers/Publishers.AppHost/Program.cs b/playground/publishers/Publishers.AppHost/Program.cs index 087b2a3e126..0aa9aa1a7c0 100644 --- a/playground/publishers/Publishers.AppHost/Program.cs +++ b/playground/publishers/Publishers.AppHost/Program.cs @@ -47,6 +47,8 @@ .WithEnvironment("P3", param3) .WithReference(backend).WaitFor(backend); +builder.AddDockerfile("mycontainer", "qots"); + #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code diff --git a/playground/publishers/Publishers.AppHost/qots/Dockerfile b/playground/publishers/Publishers.AppHost/qots/Dockerfile new file mode 100644 index 00000000000..571b7aa9e44 --- /dev/null +++ b/playground/publishers/Publishers.AppHost/qots/Dockerfile @@ -0,0 +1,12 @@ +# Stage 1: Build the Go program +ARG GO_VERSION=1.23 +FROM mcr.microsoft.com/oss/go/microsoft/golang:${GO_VERSION} AS builder +WORKDIR /app +COPY . . +RUN go build qots.go + +# Stage 2: Run the Go program +FROM mcr.microsoft.com/cbl-mariner/base/core:2.0 +WORKDIR /app +COPY --from=builder /app/qots . +CMD ["./qots"] diff --git a/playground/publishers/Publishers.AppHost/qots/qots.go b/playground/publishers/Publishers.AppHost/qots/qots.go new file mode 100644 index 00000000000..8e990937811 --- /dev/null +++ b/playground/publishers/Publishers.AppHost/qots/qots.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "math/rand" + "runtime" + "time" +) + +func main() { + fmt.Println("Go runtime version:", runtime.Version()) + + quotes := []string{ + "With great power comes great responsibility. - Spider-Man", + "I'm Batman. - Batman", + "I am Iron Man. - Iron Man", + "Why so serious? - The Joker", + "I'm always angry. - The Hulk", + } + + rand.Seed(time.Now().UnixNano()) + + for { + quote := quotes[rand.Intn(len(quotes))] + fmt.Println(quote) + time.Sleep(time.Second) + } +} diff --git a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs index 0de4357992e..34b303291d0 100644 --- a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs +++ b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs @@ -261,7 +261,7 @@ private bool TryGetContainerImageName(IResource resourceInstance, out string? co $"{resourceInstance.Name}:latest"); containerImageName = $"${{{imageEnvName}}}"; - return false; + return true; } return resourceInstance.TryGetContainerImageName(out containerImageName); From 9a08dad52bd3903add0950c663886bb5235739fb Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 4 Apr 2025 09:15:01 +0000 Subject: [PATCH 5/6] Improves publisher failure UX. --- src/Aspire.Cli/Commands/PublishCommand.cs | 49 ++++++++++++++++++----- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 5367d99776d..88f5e402e0f 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -144,7 +144,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var backchannelCompletionSource = new TaskCompletionSource(); - var launchingAppHostTask = context.AddTask("Launching apphost"); + var launchingAppHostTask = context.AddTask(":play_button: Launching apphost"); launchingAppHostTask.IsIndeterminate(); launchingAppHostTask.StartTask(); @@ -159,6 +159,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); + launchingAppHostTask.Description = $":check_mark: Launching apphost"; launchingAppHostTask.Value = 100; launchingAppHostTask.StopTask(); @@ -176,37 +177,67 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell progressTasks.Add(publishingActivity.Id, progressTask); } - progressTask.Description = $"{publishingActivity.StatusText}"; + progressTask.Description = $":play_button: {publishingActivity.StatusText}"; - if (publishingActivity.IsComplete) + if (publishingActivity.IsComplete && !publishingActivity.IsError) { + progressTask.Description = $":check_mark: {publishingActivity.StatusText}"; progressTask.Value = 100; progressTask.StopTask(); } else if (publishingActivity.IsError) { - progressTask.Value = 100; - progressTask.StopTask(); + progressTask.Description = $"[red bold]:cross_mark: {publishingActivity.StatusText}[/]"; + progressTask.Value = 0; + break; + } + else + { + // Keep going man! } } + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + // When we are running in publish mode we don't want the app host to // stop itself while we might still be streaming data back across // the RPC backchannel. So we need to take responsibility for stopping // the app host. If the CLI exits/crashes without explicitly stopping // the app host the orphan detector in the app host will kick in. - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - return await pendingRun; + if (progressTasks.Any(kvp => !kvp.Value.IsFinished)) + { + // Depending on the failure the publisher may return a zero + // exit code. + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingRun; + + // If we are in the state where we've detected an error because there + // is an incomplete task then we stop the app host, but depending on + // where/how the failure occured, we might still get a zero exit + // code. If we get a non-zero exit code we want to return that + // as it might be useful for diagnostic purposes, however if we don't + // get a non-zero exit code we want to return our built-in exit code + // for failed artifact build. + return exitCode == 0 ? ExitCodeConstants.FailedToBuildArtifacts : exitCode; + } + else + { + // If we are here then all the tasks are finished and we can + // stop the app host. + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingRun; + return exitCode; // shoudl be zero for orderly shutdown but we pass it along anyway. + } }); if (exitCode != 0) { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The build failed with exit code {exitCode}. For more information run with --debug switch.[/]"); + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Publishing artifacts failed with exit code {exitCode}. For more information run with --debug switch.[/]"); return ExitCodeConstants.FailedToBuildArtifacts; } else { - AnsiConsole.MarkupLine($"[green bold]:thumbs_up: The build completed successfully to: {fullyQualifiedOutputPath}[/]"); + AnsiConsole.MarkupLine($"[green bold]:thumbs_up: Successfully published artifacts to: {fullyQualifiedOutputPath}[/]"); return ExitCodeConstants.Success; } } From e68f9f6ded57b1a066b33e416e0fb828a5b82c44 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 4 Apr 2025 20:22:46 +1100 Subject: [PATCH 6/6] Update src/Aspire.Cli/Commands/PublishCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Commands/PublishCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 88f5e402e0f..6e03627cdf2 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -226,7 +226,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell // stop the app host. await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); var exitCode = await pendingRun; - return exitCode; // shoudl be zero for orderly shutdown but we pass it along anyway. + return exitCode; // should be zero for orderly shutdown but we pass it along anyway. } });