Skip to content

Commit a19905e

Browse files
CopilotdavidfowlMitch Denny
authored
Fix: CLI commands return incorrect exit codes when AppHost fails (#12923)
* Initial plan * Fix aspire deploy exit code bug - remove Environment.Exit from backchannel disconnect handler Co-authored-by: davidfowl <[email protected]> * Remove placeholder issue URL from comment Co-authored-by: davidfowl <[email protected]> * Use apphost exit code directly and remove unnecessary await Co-authored-by: davidfowl <[email protected]> * Tweaks to allow correct error code reporting on a rude exit from app host. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: davidfowl <[email protected]> Co-authored-by: Mitch Denny <[email protected]>
1 parent 3aa8542 commit a19905e

18 files changed

+243
-14
lines changed

src/Aspire.Cli/Commands/PipelineCommandBase.cs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Aspire.Cli.Utils;
1515
using Aspire.Hosting;
1616
using Spectre.Console;
17+
using StreamJsonRpc;
1718

1819
namespace Aspire.Cli.Commands;
1920

@@ -101,6 +102,9 @@ protected PipelineCommandBase(string name, string description, IDotNetCliRunner
101102

102103
protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
103104
{
105+
var debugMode = parseResult.GetValue<bool?>("--debug") ?? false;
106+
Task<int>? pendingRun = null;
107+
104108
// Send terminal infinite progress bar start sequence
105109
StartTerminalProgressBar();
106110

@@ -199,7 +203,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
199203

200204
var unmatchedTokens = parseResult.UnmatchedTokens.ToArray();
201205

202-
var pendingRun = _runner.RunAsync(
206+
pendingRun = _runner.RunAsync(
203207
effectiveAppHostFile,
204208
false,
205209
true,
@@ -222,8 +226,6 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
222226
});
223227

224228
var publishingActivities = backchannel.GetPublishingActivitiesAsync(cancellationToken);
225-
226-
var debugMode = parseResult.GetValue<bool?>("--debug") ?? false;
227229

228230
// Check if debug or trace logging is enabled
229231
var logLevel = parseResult.GetValue(_logLevelOption);
@@ -242,17 +244,30 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
242244
await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false);
243245
var exitCode = await pendingRun;
244246

245-
if (exitCode == 0 && noFailuresReported)
247+
// If the apphost returned a non-zero exit code, use it directly.
248+
// This ensures we properly propagate apphost failures (e.g., exceptions, crashes).
249+
if (exitCode != 0)
246250
{
247-
return ExitCodeConstants.Success;
251+
if (debugMode)
252+
{
253+
InteractionService.DisplayLines(operationOutputCollector.GetLines());
254+
}
255+
return exitCode;
248256
}
249257

250-
if (debugMode)
258+
// If the apphost exited successfully (0) but reported failures via backchannel,
259+
// return a failure exit code.
260+
if (!noFailuresReported)
251261
{
252-
InteractionService.DisplayLines(operationOutputCollector.GetLines());
262+
if (debugMode)
263+
{
264+
InteractionService.DisplayLines(operationOutputCollector.GetLines());
265+
}
266+
return ExitCodeConstants.FailedToBuildArtifacts;
253267
}
254268

255-
return ExitCodeConstants.FailedToBuildArtifacts;
269+
// Both apphost exit code and backchannel indicate success
270+
return ExitCodeConstants.Success;
256271
}
257272
catch (OperationCanceledException)
258273
{
@@ -284,6 +299,14 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
284299
InteractionService.DisplayLines(operationOutputCollector.GetLines());
285300
return ExitCodeConstants.FailedToBuildArtifacts;
286301
}
302+
catch (ConnectionLostException ex)
303+
{
304+
// Occurs if the apphost RPC channel is lost unexpectedly.
305+
StopTerminalProgressBar();
306+
InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.AppHostConnectionLost, ex.Message));
307+
InteractionService.DisplayLines(operationOutputCollector.GetLines());
308+
return pendingRun is { } && debugMode ? await pendingRun : ExitCodeConstants.FailedToBuildArtifacts;
309+
}
287310
catch (Exception ex)
288311
{
289312
// Send terminal progress bar stop sequence on exception

src/Aspire.Cli/DotNet/DotNetCliRunner.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -690,11 +690,10 @@ private async Task StartBackchannelAsync(Process? process, string socketPath, Ta
690690
logger.LogTrace("Attempting to connect to AppHost backchannel at {SocketPath} (attempt {Attempt})", socketPath, connectionAttempts++);
691691
await backchannel.ConnectAsync(socketPath, cancellationToken).ConfigureAwait(false);
692692
backchannelCompletionSource.SetResult(backchannel);
693-
backchannel.AddDisconnectHandler((_, _) =>
694-
{
695-
// If the backchannel disconnects, we want to stop the CLI process
696-
Environment.Exit(ExitCodeConstants.Success);
697-
});
693+
// Note: We intentionally do not call Environment.Exit when the backchannel disconnects.
694+
// The CLI should complete normally and return the appropriate exit code based on the
695+
// deployment result. Calling Environment.Exit here would bypass the normal exit code
696+
// logic and always return success (0), even when the deployment failed.
698697

699698
logger.LogDebug("Connected to AppHost backchannel at {SocketPath}", socketPath);
700699
return;

src/Aspire.Cli/Resources/InteractionServiceStrings.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Aspire.Cli/Resources/InteractionServiceStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@
193193
<data name="ErrorConnectingToAppHost" xml:space="preserve">
194194
<value>An error occurred while connecting to the app host. The app host possibly crashed before it was available: {0}.</value>
195195
</data>
196+
<data name="AppHostConnectionLost" xml:space="preserve">
197+
<value>The connection to the app host was lost: {0}.</value>
198+
</data>
196199
<data name="UnexpectedErrorOccurred" xml:space="preserve">
197200
<value>An unexpected error occurred: {0}</value>
198201
<comment>{0} is the exception message</comment>

src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.es.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.fr.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.it.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ja.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)