Skip to content

Add 'aspire wait' CLI command#14419

Merged
sebastienros merged 8 commits intomainfrom
feature/aspire-wait-command
Feb 11, 2026
Merged

Add 'aspire wait' CLI command#14419
sebastienros merged 8 commits intomainfrom
feature/aspire-wait-command

Conversation

@sebastienros
Copy link
Member

Summary

Adds a new aspire wait CLI 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 after aspire run --detach.

Usage

# Wait for a resource to be healthy (default)
aspire wait webfrontend

# Wait for a resource to be running
aspire wait mydb --status up

# Wait for a resource to stop
aspire wait worker --status down

# With custom timeout (seconds)
aspire wait webfrontend --status healthy --timeout 60

# Target specific AppHost project
aspire wait mydb --project ./MyApp.AppHost/MyApp.AppHost.csproj

--status values

Value Condition
healthy (default) Running + Healthy, or Running with no health checks configured
up Running (regardless of health status)
down Finished, Exited, or FailedToStart

Exit codes

Code Meaning
0 Resource reached target status
7 No running AppHost found
17 Timeout exceeded
18 Resource entered a failed/terminal state while waiting for up/healthy

Implementation details

  • Uses WatchResourceSnapshotsAsync from the AppHost backchannel to stream resource state changes in real-time
  • Validates resource exists upfront via GetResourceSnapshotsAsync before entering the wait loop (avoids silent timeout on typos)
  • Distinguishes "no health checks configured" (HealthReports empty) from "health checks pending" (HealthReports exist but status is null) to avoid race conditions
  • Supports case-insensitive --status values
  • Uses TimeProvider for testable time measurement
  • Early exit with code 18 when resource enters FailedToStart or RuntimeUnhealthy state
  • Follows existing CLI command patterns (BaseCommand, AppHostConnectionResolver, .resx localization)

Changes

New files

  • src/Aspire.Cli/Commands/WaitCommand.cs — Command implementation
  • src/Aspire.Cli/Resources/WaitCommandStrings.resx + .Designer.cs — Localized strings
  • src/Aspire.Cli/Resources/xlf/WaitCommandStrings.*.xlf — 13 translation files
  • tests/Aspire.Cli.Tests/Commands/WaitCommandTests.cs — 19 unit tests
  • tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs — E2E test

Modified files

  • src/Aspire.Cli/ExitCodeConstants.cs — Added WaitTimeout = 17, WaitResourceFailed = 18
  • src/Aspire.Cli/Program.cs — DI registration
  • src/Aspire.Cli/Commands/RootCommand.cs — Subcommand registration
  • src/Aspire.Cli/README.md — Documentation
  • tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs — Test DI registration

…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
Copilot AI review requested due to automatic review settings February 10, 2026 00:17
@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14419

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14419"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 WaitCommand with --status, --timeout, and --project options, 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

Comment on lines 129 to 134
if (!initialSnapshots.Any(s => string.Equals(s.Name, resourceName, StringComparison.OrdinalIgnoreCase)))
{
_interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, WaitCommandStrings.ResourceNotFound, resourceName));
return ExitCodeConstants.WaitResourceFailed;
}

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines 176 to 177
// Stream ended without reaching target status
return ExitCodeConstants.WaitTimeout;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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;

Copilot uses AI. Check for mistakes.
// 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));
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
_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);

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit a9c02b9:

Test Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateStartWaitAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
ResourcesCommandShowsRunningResources ▶️ View Recording

📹 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)
@davidfowl
Copy link
Member

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.

@sebastienros sebastienros reopened this Feb 11, 2026
@dotnet-policy-service dotnet-policy-service bot added this to the 13.2 milestone Feb 11, 2026
Copy link
Member

@mitchdenny mitchdenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

@mitchdenny mitchdenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the server-side wait design is solid and the nits above can be addressed in a follow-up.

@sebastienros sebastienros force-pushed the feature/aspire-wait-command branch from f23ba21 to a9c02b9 Compare February 11, 2026 15:51
@sebastienros sebastienros merged commit bdd9815 into main Feb 11, 2026
673 of 677 checks passed
@sebastienros sebastienros deleted the feature/aspire-wait-command branch February 11, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants