Conversation
…get status Adds a new CLI command 'aspire wait <resource>' that connects to a running AppHost and blocks until the specified resource reaches the desired status or a timeout is exceeded. Options: --status <healthy|up|down> Target status (default: healthy) --timeout <seconds> Maximum wait time (default: 120) --project <path> Path to AppHost project file Exit codes: 0 - Resource reached desired status 17 - Timeout exceeded 18 - Resource entered terminal failure state 7 - Failed to find/connect to AppHost
…nd detection, case-insensitive status, TimeProvider usage - Check HealthReports to distinguish 'no health checks' from 'pending health checks' - Validate resource exists via GetResourceSnapshotsAsync before entering wait loop - Normalize --status input to lowercase for case-insensitive matching - Use TimeProvider.GetTimestamp/GetElapsedTime instead of Stopwatch - Add 7 new functional tests covering all fixed scenarios
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14419Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14419" |
There was a problem hiding this comment.
Pull request overview
Adds a new aspire wait CLI command to block until a named resource reaches a target status (healthy/up/down) with a configurable timeout—supporting CI/CD and agentic workflows after aspire run --detach.
Changes:
- Introduces
WaitCommandwith--status,--timeout, and--projectoptions, plus new exit codes for timeout/failure. - Adds localized resources (.resx + .xlf) and wires the command into CLI DI + root command registration.
- Adds unit and end-to-end test coverage and updates CLI documentation.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | Registers WaitCommand in test DI container. |
| tests/Aspire.Cli.Tests/Commands/WaitCommandTests.cs | Adds unit tests for parsing/options and basic wait behaviors. |
| tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs | Adds an E2E scenario exercising aspire new, run --detach, wait, and stop. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.zh-Hant.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.zh-Hans.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.tr.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.ru.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.pt-BR.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.pl.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.ko.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.ja.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.it.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.fr.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.es.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.de.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/xlf/WaitCommandStrings.cs.xlf | New localization file for wait command strings. |
| src/Aspire.Cli/Resources/WaitCommandStrings.resx | Adds the base localized strings used by WaitCommand. |
| src/Aspire.Cli/Resources/WaitCommandStrings.Designer.cs | Auto-generated strongly-typed resource wrapper for WaitCommandStrings.resx. |
| src/Aspire.Cli/README.md | Documents aspire wait usage, status values, and exit codes. |
| src/Aspire.Cli/Program.cs | Registers WaitCommand in production DI container. |
| src/Aspire.Cli/ExitCodeConstants.cs | Adds WaitTimeout = 17 and WaitResourceFailed = 18. |
| src/Aspire.Cli/Commands/WaitCommand.cs | Implements the new wait subcommand logic. |
| src/Aspire.Cli/Commands/RootCommand.cs | Adds wait to the CLI’s subcommand set. |
Files not reviewed (1)
- src/Aspire.Cli/Resources/WaitCommandStrings.Designer.cs: Language not supported
| if (!initialSnapshots.Any(s => string.Equals(s.Name, resourceName, StringComparison.OrdinalIgnoreCase))) | ||
| { | ||
| _interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, WaitCommandStrings.ResourceNotFound, resourceName)); | ||
| return ExitCodeConstants.WaitResourceFailed; | ||
| } | ||
|
|
There was a problem hiding this comment.
WatchResourceSnapshotsAsync streams changes (it doesn’t include the current snapshot on subscription), but this command only verifies existence via GetResourceSnapshotsAsync and then waits exclusively on the watch stream. If the resource is already in the target state (e.g., Running/Healthy) and no further changes occur, aspire wait can block until timeout even though the condition is already satisfied. Consider evaluating the current snapshot (from initialSnapshots) with IsTargetStatusReached and returning success immediately before starting the watch loop (and similarly for --status down).
| if (!initialSnapshots.Any(s => string.Equals(s.Name, resourceName, StringComparison.OrdinalIgnoreCase))) | |
| { | |
| _interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, WaitCommandStrings.ResourceNotFound, resourceName)); | |
| return ExitCodeConstants.WaitResourceFailed; | |
| } | |
| var initialSnapshot = initialSnapshots.FirstOrDefault(s => string.Equals(s.Name, resourceName, StringComparison.OrdinalIgnoreCase)); | |
| if (initialSnapshot is null) | |
| { | |
| _interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, WaitCommandStrings.ResourceNotFound, resourceName)); | |
| return ExitCodeConstants.WaitResourceFailed; | |
| } | |
| // If the resource has already reached the target status, return success immediately | |
| if (IsTargetStatusReached(initialSnapshot, status)) | |
| { | |
| _interactionService.DisplaySuccess(string.Format( | |
| CultureInfo.CurrentCulture, | |
| WaitCommandStrings.ResourceReachedTargetStatus, | |
| resourceName, | |
| statusLabel, | |
| 0)); | |
| return ExitCodeConstants.Success; | |
| } |
| // Stream ended without reaching target status | ||
| return ExitCodeConstants.WaitTimeout; |
There was a problem hiding this comment.
When the watch stream completes (e.g., AppHost disconnects or the remote AppHost doesn’t implement WatchResourceSnapshotsAsync and the backchannel yields no items), the code returns WaitTimeout without emitting the timeout error message, which is misleading because no timeout necessarily occurred. Consider treating an ended/empty stream as a distinct failure (and displaying a specific error) rather than WaitTimeout, or ensure the timeout message is shown only when the actual timeout elapses.
| // Stream ended without reaching target status | |
| return ExitCodeConstants.WaitTimeout; | |
| // Stream ended without reaching target status; this is not a timeout. | |
| _interactionService.DisplayError(string.Format( | |
| CultureInfo.CurrentCulture, | |
| "The watch for resource '{0}' ended before reaching status '{1}'. This may indicate that the AppHost disconnected or does not support watching resource snapshots.", | |
| resourceName, | |
| statusLabel)); | |
| return ExitCodeConstants.WaitResourceFailed; |
| // When waiting for "healthy" or "up", check if the resource exited | ||
| if (status is not "down" && IsTerminalState(snapshot)) | ||
| { | ||
| _interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, WaitCommandStrings.ResourceEnteredFailedState, resourceName, snapshot.State)); |
There was a problem hiding this comment.
ResourceEnteredFailedState is used both for genuinely failed states and for normal terminal states like Exited/Finished (see the IsTerminalState branch). This produces an inaccurate error message (a process can exit successfully). Consider adding a separate localized string for “entered a terminal/stopped state” (optionally including exit code) and using it when the resource stops while waiting for up/healthy.
| _interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, WaitCommandStrings.ResourceEnteredFailedState, resourceName, snapshot.State)); | |
| var message = string.Format( | |
| CultureInfo.CurrentCulture, | |
| "Resource '{0}' entered terminal state '{1}' before reaching the requested status.", | |
| resourceName, | |
| snapshot.State); | |
| _interactionService.DisplayError(message); |
🎬 CLI E2E Test RecordingsThe following terminal recordings are available for commit
📹 Recordings uploaded automatically from CI run #21912175702 |
…apshot check The upfront GetResourceSnapshotsAsync check fails when the AppHost has just started and resources haven't been reported to the backchannel yet. Instead, track whether the resource appears in the watch stream and report not-found only when the stream completes without ever seeing the target resource.
… --status up in E2E test
- Match user-provided resource name against both snapshot.Name (ResourceId like
'webfrontend-abc123') and snapshot.DisplayName ('webfrontend')
- Switch E2E test to --status up for reliability (avoids health check dependencies)
|
I'm not sure this implementation should be in the CLI, I think we want to push this to the apphost since we'll evolve the terminal states over time and there are constants there for this stuff. |
mitchdenny
left a comment
There was a problem hiding this comment.
Nice work — clean design with the server-side wait. Two observations:
1. WaitForRunningAsync doesn't use StopOnResourceUnavailable
WaitForHealthyAsync correctly uses WaitBehavior.StopOnResourceUnavailable so it fails fast when a resource enters FailedToStart or RuntimeUnhealthy. But WaitForRunningAsync uses a raw predicate that only matches Running — if the resource enters FailedToStart, aspire wait mydb --status up will block until timeout instead of exiting early with code 18. Worth aligning the behavior, or at least adding a terminal-state check to the predicate so it can early-exit.
2. Consider a --quiet flag for CI/CD usage
For scripting and agentic workflows (which is the primary use case per the PR description), a --quiet mode that suppresses the spinner and status messages would be useful — callers would rely purely on the exit code. Not a blocker for this PR, but worth considering as a fast follow.
mitchdenny
left a comment
There was a problem hiding this comment.
Approving — the server-side wait design is solid and the nits above can be addressed in a follow-up.
f23ba21 to
a9c02b9
Compare
Summary
Adds a new
aspire waitCLI command that blocks until a named resource reaches a target status, with a configurable timeout. This enables agentic and CI/CD workflows where callers need to wait for resources to be ready afteraspire run --detach.Usage
--statusvalueshealthy(default)updownExit codes
Implementation details
WatchResourceSnapshotsAsyncfrom the AppHost backchannel to stream resource state changes in real-timeGetResourceSnapshotsAsyncbefore entering the wait loop (avoids silent timeout on typos)--statusvaluesTimeProviderfor testable time measurementFailedToStartorRuntimeUnhealthystateChanges
New files
src/Aspire.Cli/Commands/WaitCommand.cs— Command implementationsrc/Aspire.Cli/Resources/WaitCommandStrings.resx+.Designer.cs— Localized stringssrc/Aspire.Cli/Resources/xlf/WaitCommandStrings.*.xlf— 13 translation filestests/Aspire.Cli.Tests/Commands/WaitCommandTests.cs— 19 unit teststests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs— E2E testModified files
src/Aspire.Cli/ExitCodeConstants.cs— AddedWaitTimeout = 17,WaitResourceFailed = 18src/Aspire.Cli/Program.cs— DI registrationsrc/Aspire.Cli/Commands/RootCommand.cs— Subcommand registrationsrc/Aspire.Cli/README.md— Documentationtests/Aspire.Cli.Tests/Utils/CliTestHelper.cs— Test DI registration