Skip to content

Redirect human-readable messages to stderr when --format json is used#14572

Merged
mitchdenny merged 8 commits intorelease/13.2from
fix/detach-json-stdout-contamination
Feb 24, 2026
Merged

Redirect human-readable messages to stderr when --format json is used#14572
mitchdenny merged 8 commits intorelease/13.2from
fix/detach-json-stdout-contamination

Conversation

@mitchdenny
Copy link
Copy Markdown
Member

Summary

When running aspire run --detach --format json, human-readable messages like "Finding apphosts..." and "Stopping previous instance..." were written to stdout alongside the JSON output, making it impossible to parse the JSON programmatically.

Changes

  • Added UseStderrForMessages property to IInteractionService with a default no-op implementation
  • ConsoleInteractionService routes all human-readable display methods (DisplayMessage, DisplaySuccess, DisplayError, ShowStatusAsync, etc.) through a MessageConsole property that returns _errorConsole (stderr) when the flag is set
  • DisplayRawText remains on stdout — this is used for the structured JSON output
  • ExecuteDetachedAsync in RunCommand sets UseStderrForMessages = true when --format json is specified

Before

$ aspire run --detach --format json
🔍 Finding apphosts...
apphost.cs

🛑 Stopping previous instance (AppHost PID: 89136, CLI PID: 15160)
✔  Running instance stopped successfully.
{
  "appHostPath": "...",
  ...
}

After

$ aspire run --detach --format json
{"appHostPath":"...","appHostPid":70740,"cliPid":64920,...}

Human-readable messages still appear on stderr so users can see progress when running interactively, but stdout | jq now works correctly.

Fixes #14423

When running `aspire run --detach --format json`, messages like
"Finding apphosts..." and "Stopping previous instance..." were written
to stdout alongside the JSON output, making it impossible to parse the
JSON programmatically.

Add a `UseStderrForMessages` property to IInteractionService. When
enabled, ConsoleInteractionService routes all human-readable display
methods through stderr while keeping DisplayRawText on stdout for
structured output. The property is set in ExecuteDetachedAsync when
the output format is JSON.

Fixes #14423

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 19, 2026 07:23
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 19, 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 -- 14572

Or

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

Copy link
Copy Markdown
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

This pull request fixes issue #14423 where human-readable messages were being written to stdout alongside JSON output when running aspire run --detach --format json, making the JSON output unparseable by automated tools.

Changes:

  • Added UseStderrForMessages property to IInteractionService to enable routing human-readable messages to stderr
  • Modified ConsoleInteractionService to route display methods through a MessageConsole property that returns stderr when the flag is set
  • Set UseStderrForMessages = true in RunCommand.ExecuteDetachedAsync when --format json is specified

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/Aspire.Cli/Interaction/IInteractionService.cs Added UseStderrForMessages property with default no-op implementation
src/Aspire.Cli/Interaction/ConsoleInteractionService.cs Implemented UseStderrForMessages property and routed all message display methods through MessageConsole that redirects to stderr when flag is set; DisplayRawText correctly stays on stdout
src/Aspire.Cli/Commands/RunCommand.cs Sets UseStderrForMessages = true when running in detached mode with JSON format

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 19, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit 7c2b827:

Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AgentInitCommand_WithMalformedMcpJson_ShowsErrorAndExitsNonZero ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ 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
DetachFormatJsonProducesValidJson ▶️ 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
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ❌ Upload failed
StopNonInteractiveMultipleAppHostsShowsError ❌ Upload failed
StopNonInteractiveSingleAppHost ❌ Upload failed
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording

📹 Recordings uploaded automatically from CI run #22335500927

…enum

- Add ConsoleOutput enum (Standard, Error) for explicit console targeting
- Replace DIM UseStderrForMessages with standard DefaultConsole property
- Add optional ConsoleOutput parameter to DisplayRawText for explicit targeting
- Update all IInteractionService implementations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny
Copy link
Copy Markdown
Member Author

/deployment-test

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Deployment tests starting on PR #14572...

This will deploy to real Azure infrastructure. Results will be posted here when complete.

View workflow run

@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 20, 2026 02:11 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions github-actions bot temporarily deployed to deployment-testing February 23, 2026 23:35 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

Deployment E2E Tests passed

Summary: 21 passed, 0 failed, 0 cancelled

View workflow run

Passed Tests

  • ✅ AcaCompactNamingUpgradeDeploymentTests
  • ✅ AksStarterWithRedisDeploymentTests
  • ✅ AppServicePythonDeploymentTests
  • ✅ AcaCompactNamingDeploymentTests
  • ✅ AzureAppConfigDeploymentTests
  • ✅ AzureContainerRegistryDeploymentTests
  • ✅ VnetStorageBlobConnectivityDeploymentTests
  • ✅ PythonFastApiDeploymentTests
  • ✅ AppServiceReactDeploymentTests
  • ✅ AuthenticationTests
  • ✅ AzureEventHubsDeploymentTests
  • ✅ AzureServiceBusDeploymentTests
  • ✅ VnetStorageBlobInfraDeploymentTests
  • ✅ VnetKeyVaultInfraDeploymentTests
  • ✅ VnetKeyVaultConnectivityDeploymentTests
  • ✅ AksStarterDeploymentTests
  • ✅ VnetSqlServerInfraDeploymentTests
  • ✅ AzureStorageDeploymentTests
  • ✅ AzureLogAnalyticsDeploymentTests
  • ✅ AcaStarterDeploymentTests
  • ✅ AzureKeyVaultDeploymentTests

🎬 Terminal Recordings

Test Recording
DeployAzureAppConfigResource ▶️ View Recording
DeployAzureContainerRegistryResource ▶️ View Recording
DeployAzureEventHubsResource ▶️ View Recording
DeployAzureKeyVaultResource ▶️ View Recording
DeployAzureLogAnalyticsResource ▶️ View Recording
DeployAzureServiceBusResource ▶️ View Recording
DeployAzureStorageResource ▶️ View Recording
DeployPythonFastApiTemplateToAzureAppService ▶️ View Recording
DeployPythonFastApiTemplateToAzureContainerApps ▶️ View Recording
DeployReactTemplateToAzureAppService ▶️ View Recording
DeployStarterTemplateToAks ▶️ View Recording
DeployStarterTemplateToAzureContainerApps ▶️ View Recording
DeployStarterTemplateWithKeyVaultPrivateEndpoint ▶️ View Recording
DeployStarterTemplateWithRedisToAks ▶️ View Recording
DeployStarterTemplateWithStorageBlobPrivateEndpoint ▶️ View Recording
DeployVnetKeyVaultInfrastructure ▶️ View Recording
DeployVnetSqlServerInfrastructure ▶️ View Recording
DeployVnetStorageBlobInfrastructure ▶️ View Recording
DeployWithCompactNamingFixesStorageCollision ▶️ View Recording
UpgradeFromGaToDevDoesNotDuplicateStorageAccounts ▶️ View Recording

Mitch Denny and others added 2 commits February 24, 2026 11:25
Instead of testing multiple-apphost selection prompts, the test now
validates the core PR concern: that aspire run --detach --format json
produces well-formed JSON on stdout without human-readable message
pollution.

The test creates a single project, runs aspire run --detach first,
then runs aspire run --detach --format json > output.json and validates
the output file is parseable JSON with expected fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The WaitUntil was looking for 'The apphost is running in the
background.' which doesn't exist. The actual output is the AppHost
summary table followed by the shell prompt. Use WaitForSuccessPrompt
directly which correctly waits for the [N OK] $ prompt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny
Copy link
Copy Markdown
Member Author

@joperezr this should be good to go now.

@mitchdenny mitchdenny enabled auto-merge (squash) February 24, 2026 02:16
@JamesNK
Copy link
Copy Markdown
Member

JamesNK commented Feb 24, 2026

My review comments haven't been addressed: #14572 (review)

mitchdenny and others added 2 commits February 24, 2026 14:00
Co-authored-by: James Newton-King <james@newtonking.com>
- Rename DefaultConsole to Console per review request
- Change DisplayRawText parameter from ConsoleOutput to ConsoleOutput?
  (nullable). When null, respects the Console property setting. Renamed
  parameter to consoleOverride to indicate it bypasses the default.
- Audit all DisplayRawText callers: all output structured data (JSON)
  that must always go to stdout, so explicitly pass ConsoleOutput.Standard
- Fix System.Console shadowing in test file caused by Console rename

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny merged commit 1cd0225 into release/13.2 Feb 24, 2026
345 of 346 checks passed
@mitchdenny mitchdenny deleted the fix/detach-json-stdout-contamination branch February 24, 2026 04:16
@dotnet-policy-service dotnet-policy-service bot added this to the 13.2 milestone Feb 24, 2026
Copilot AI pushed a commit that referenced this pull request Mar 10, 2026
…#14572)

* Redirect human-readable messages to stderr when --format json is used

When running `aspire run --detach --format json`, messages like
"Finding apphosts..." and "Stopping previous instance..." were written
to stdout alongside the JSON output, making it impossible to parse the
JSON programmatically.

Add a `UseStderrForMessages` property to IInteractionService. When
enabled, ConsoleInteractionService routes all human-readable display
methods through stderr while keeping DisplayRawText on stdout for
structured output. The property is set in ExecuteDetachedAsync when
the output format is JSON.

Fixes #14423

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR feedback: replace UseStderrForMessages with ConsoleOutput enum

- Add ConsoleOutput enum (Standard, Error) for explicit console targeting
- Replace DIM UseStderrForMessages with standard DefaultConsole property
- Add optional ConsoleOutput parameter to DisplayRawText for explicit targeting
- Update all IInteractionService implementations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use Throw for multiple apphosts when --format json; add E2E test

When --format json is used with aspire run --detach, use
MultipleAppHostProjectsFoundBehavior.Throw instead of Prompt to prevent
interactive selection UI from polluting stdout JSON output. Users should
specify --project explicitly in this case.

Added MultipleAppHostTests E2E test that creates two single-file apphosts
and verifies the selection prompt appears when multiple are found.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix MultipleAppHostTests: use real aspire new projects and assert on errors

The previous test used stub single-file apphosts that couldn't resolve
the Aspire.AppHost.Sdk, so the selection prompt appeared but the
selected apphost failed to build. The test passed vacuously.

Now creates two real projects via aspire new, verifies the selection
prompt appears, selects one, and asserts the apphost actually starts.
Adds explicit error assertions for SDK resolution and missing project
failures to fail fast with descriptive messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rewrite test: validate --detach --format json produces valid JSON

Instead of testing multiple-apphost selection prompts, the test now
validates the core PR concern: that aspire run --detach --format json
produces well-formed JSON on stdout without human-readable message
pollution.

The test creates a single project, runs aspire run --detach first,
then runs aspire run --detach --format json > output.json and validates
the output file is parseable JSON with expected fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix detach test: wait for prompt, not nonexistent message

The WaitUntil was looking for 'The apphost is running in the
background.' which doesn't exist. The actual output is the AppHost
summary table followed by the shell prompt. Use WaitForSuccessPrompt
directly which correctly waits for the [N OK] $ prompt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Mitch Denny <mitch@mitchdeny.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions bot locked and limited conversation to collaborators Mar 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

aspire run --detach --format json still dumps other outputs to stdout making it hard to parse the json

4 participants