Skip to content

Add aspire do --watch for attaching to pipeline execution progress #16920

@davidfowl

Description

@davidfowl

Summary

Add a read-only way for Aspire CLI and external tools to attach to an already-running deployment/pipeline execution and observe progress, using the auxiliary AppHost backchannel.

The target CLI experience is:

aspire do --watch
aspire do --watch --format json

--watch must not start a new pipeline execution. It should attach to the current pipeline execution for the selected running AppHost.

Motivation

Today, tools can show live deployment progress only if they launch aspire do <step> or aspire deploy themselves and parse the owning CLI process output. A viewer should be able to attach after a deployment has already started, similar to how existing commands attach to a running AppHost for resources/logs.

Existing attach-style commands already use the auxiliary backchannel and AppHost discovery machinery. Deployment/pipeline execution currently uses a private CLI-owned publish backchannel, so it does not provide multi-client discovery, replay, or current execution state for late observers.

Proposed command behavior

aspire do --watch

Attach to the active pipeline execution for the selected AppHost. If there is no active execution, report that none is running.

For v1, assume a single active pipeline execution per AppHost. Running parallel deployments for the same project/AppHost is not a primary scenario, and --watch should not require users to understand or pass an execution ID.

If the AppHost ever reports multiple active executions, the CLI should treat that as an ambiguity/error in v1 rather than designing the main UX around that case.

aspire do --watch --format json

Emit a snapshot-first NDJSON event stream suitable for tools and live pipeline viewers.

Useful optional flags:

--apphost <path>       # select the running AppHost, matching existing attach commands
--since <sequence>     # resume from a known event sequence
--replay all|none|tail # default: all available replay
--tail <count>         # when replay=tail

Required data model

Add an AppHost-side pipeline execution registry exposed over the auxiliary backchannel behind a new capability, for example pipeline-observe.v1.

Each AppHost should expose its current active pipeline execution, if any. Internally, the AppHost may still use a stable execution identifier/correlation ID for event envelopes, tests, logging, and future-proofing, but the v1 CLI should not require or center the UX around passing that ID.

Each execution should have:

  • Internal correlation/execution ID
  • Operation (publish, deploy, destroy, do)
  • Target step, if any
  • Started/completed timestamps
  • Current status
  • Step graph snapshot
  • Current step/task status maps
  • Redacted input/resource metadata
  • Bounded replay buffer with monotonic sequence numbers
  • Multi-observer fan-out for live events

Attach clients are observers only. They must not answer prompts, own process lifetime, or keep the AppHost alive after the owning CLI exits.

JSON/NDJSON stream shape

aspire do --watch --format json should emit one JSON object per line.

Every object should use a stable envelope:

{
  "schema": "aspire.pipeline.event.v1",
  "sequence": 42,
  "timestamp": "2026-05-10T06:02:14.392Z",
  "execution": {
    "operation": "deploy",
    "targetStep": "deploy"
  },
  "event": {
    "type": "step",
    "id": "deploy-api",
    "status": "running"
  }
}

The first event from watch should be a full snapshot:

{
  "schema": "aspire.pipeline.event.v1",
  "sequence": 128,
  "timestamp": "2026-05-10T06:02:10.000Z",
  "execution": {
    "operation": "deploy",
    "targetStep": "deploy",
    "status": "running",
    "startedAt": "2026-05-10T06:01:50.000Z",
    "completedAt": null
  },
  "event": {
    "type": "snapshot",
    "replay": {
      "firstSequence": 94,
      "lastSequence": 128,
      "isTruncated": false
    },
    "steps": [
      {
        "id": "build-api",
        "name": "Build api",
        "resourceName": "api",
        "dependsOn": [],
        "status": "succeeded",
        "startedAt": "2026-05-10T06:01:51.000Z",
        "completedAt": "2026-05-10T06:01:58.000Z",
        "message": "Build completed"
      }
    ],
    "tasks": [],
    "inputs": [],
    "resources": []
  }
}

Expected event types:

  • snapshot
  • step
  • task
  • log
  • prompt — observe only; observerCanRespond should be false
  • complete
  • replay-truncated

Suggested normalized statuses:

  • pending
  • running
  • succeeded
  • warning
  • failed
  • skipped
  • cancelled
  • unknown

Example human output

For an interactive terminal, aspire do --watch should render a human-readable live progress view similar to the owning deployment command, but clearly in observer mode:

Watching active pipeline execution for AppHost: MyApp.AppHost
Operation: deploy
Target step: deploy

✓ Build api
✓ Build web
⠋ Deploy api
  • Pushing container image
  • Updating Azure Container App
○ Deploy web
○ Publish outputs

[12:02:16] [api] Pushed image myregistry.azurecr.io/api:latest
[12:02:19] [api] Updating revision...

When the pipeline completes:

Watching active pipeline execution for AppHost: MyApp.AppHost
Operation: deploy
Target step: deploy

✓ Build api
✓ Build web
✓ Deploy api
✓ Deploy web
✓ Publish outputs

Deployment completed in 1m 15s.

If no pipeline is running:

No active pipeline execution found for AppHost: MyApp.AppHost.

If multiple active executions are ever reported by the AppHost, v1 should fail clearly rather than requiring users to choose an execution ID:

Multiple active pipeline executions were reported for AppHost: MyApp.AppHost. Watching multiple executions is not supported yet.

Example JSON stream output

For tools, aspire do --watch --format json should emit snapshot-first NDJSON. The first line gives the viewer enough information to build the graph immediately:

{"schema":"aspire.pipeline.event.v1","sequence":12,"timestamp":"2026-05-10T06:02:10.000Z","execution":{"operation":"deploy","targetStep":"deploy","status":"running","startedAt":"2026-05-10T06:01:50.000Z","completedAt":null},"event":{"type":"snapshot","replay":{"firstSequence":1,"lastSequence":12,"isTruncated":false},"steps":[{"id":"build-api","name":"Build api","resourceName":"api","dependsOn":[],"status":"succeeded","startedAt":"2026-05-10T06:01:51.000Z","completedAt":"2026-05-10T06:01:58.000Z","message":"Build completed"},{"id":"deploy-api","name":"Deploy api","resourceName":"api","dependsOn":["build-api"],"status":"running","startedAt":"2026-05-10T06:01:59.000Z","completedAt":null,"message":"Deploying api"}],"tasks":[{"id":"deploy-api.push-image","stepId":"deploy-api","name":"Push container image","status":"running","message":"Pushing image"}],"inputs":[{"name":"subscriptionId","kind":"parameter","required":true,"hasValue":true,"isSecret":false,"value":"00000000-0000-0000-0000-000000000000"},{"name":"databasePassword","kind":"parameter","required":true,"hasValue":true,"isSecret":true,"value":null}],"resources":[{"name":"api","type":"project.v0"}]}}

Subsequent lines are incremental updates:

{"schema":"aspire.pipeline.event.v1","sequence":13,"timestamp":"2026-05-10T06:02:14.392Z","execution":{"operation":"deploy","targetStep":"deploy"},"event":{"type":"step","id":"deploy-api","name":"Deploy api","resourceName":"api","status":"running","message":"Deploying api"}}
{"schema":"aspire.pipeline.event.v1","sequence":14,"timestamp":"2026-05-10T06:02:15.102Z","execution":{"operation":"deploy","targetStep":"deploy"},"event":{"type":"task","id":"deploy-api.push-image","stepId":"deploy-api","name":"Push container image","status":"running","message":"Pushing image"}}
{"schema":"aspire.pipeline.event.v1","sequence":15,"timestamp":"2026-05-10T06:02:16.500Z","execution":{"operation":"deploy","targetStep":"deploy"},"event":{"type":"log","stepId":"deploy-api","taskId":"deploy-api.push-image","level":"information","message":"Pushed image myregistry.azurecr.io/api:latest","category":"Aspire.Hosting.Publishing"}}
{"schema":"aspire.pipeline.event.v1","sequence":16,"timestamp":"2026-05-10T06:03:05.000Z","execution":{"operation":"deploy","targetStep":"deploy","status":"succeeded","startedAt":"2026-05-10T06:01:50.000Z","completedAt":"2026-05-10T06:03:05.000Z"},"event":{"type":"complete","status":"succeeded","message":"Deployment completed","durationMs":75000}}

This shape lets a viewer build the graph from snapshot.steps[].dependsOn, update nodes from step events, update sub-operations from task events, attach log lines to the relevant step/task, and finish when it receives complete.

Tools this enables

This feature is intended to make external deployment observers possible without requiring those tools to spawn and own aspire do or parse terminal output.

Examples:

  • A dedicated live pipeline viewer, such as tjwald/aspire-pipeline-viewer, can attach to a deployment that is already running and render the step graph/statuses directly from structured events.
  • IDE extensions can show deployment progress in a tree/graph view while the deployment continues in another terminal.
  • Dashboard or local developer tools can display deployment status alongside resource/log views.
  • CI or automation wrappers can consume the NDJSON stream and produce richer progress annotations without becoming the process that starts the deployment.

Implementation notes

Likely files/classes involved:

  • src/Aspire.Hosting/Pipelines/PipelineActivityReporter.cs
  • src/Aspire.Hosting/Publishing/PipelineExecutor.cs
  • src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs
  • src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs
  • src/Aspire.Hosting/DistributedApplicationBuilder.cs
  • src/Aspire.Hosting/Backchannel/BackchannelLoggerProvider.cs as a replay/fan-out pattern to copy
  • src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs
  • src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs
  • src/Aspire.Cli/Backchannel/BackchannelJsonSerializerContext.cs
  • src/Aspire.Cli/Commands/DoCommand.cs
  • src/Aspire.Cli/Commands/PipelineCommandBase.cs

Tests should cover:

  • Execution registry snapshot/replay/fan-out
  • Status normalization
  • Replay truncation behavior
  • Auxiliary RPC capability and watch behavior
  • Serializer context coverage
  • aspire do --watch attach behavior
  • JSON/NDJSON output shape
  • No-active-execution behavior
  • Multiple-active-execution ambiguity/error behavior, if the server can report that state

Compatibility and lifecycle constraints

  • Follow the backchannel compatibility guidance in docs/specs/cli-backchannel.md.
  • Use additive request/response DTOs and capability negotiation.
  • Preserve old/new CLI-AppHost compatibility.
  • Do not expose secret input values.
  • Do not make attach clients owners of the deployment process.
  • Do not replay prompts as actionable prompts.
  • Bounded replay must report truncation so viewers can show partial history honestly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions