Skip to content

Version the AppHost build-ownership capability so no launch runs stale output - #19132

Closed
Adam Ratzman (adamint) wants to merge 11 commits into
microsoft:mainfrom
adamint:adamint/issue15850-build-before-run
Closed

Version the AppHost build-ownership capability so no launch runs stale output#19132
Adam Ratzman (adamint) wants to merge 11 commits into
microsoft:mainfrom
adamint:adamint/issue15850-build-before-run

Conversation

@adamint

@adamint Adam Ratzman (adamint) commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Versions the CLI/VS Code build-ownership capability so an AppHost launch never skips the build because both sides believed the other side owned it.

Fixes #15850.

Changes

  • Replaces the ambiguous build-dotnet-using-cli token with the exact build-dotnet-using-cli.v2 contract on both sides of the backchannel.
  • A current CLI builds only when the extension advertises v2; older extensions keep owning their existing build path.
  • A current extension skips its build only when the CLI advertises v2; the legacy token is not accepted because affected CLI versions did not pre-build every no-debug launch.
  • Covers legacy, v2, unknown, debug, and no-debug combinations.

The E2E shard-matrix extraction is intentionally not part of this PR.

Validation

VS Code extension build-ownership tests: 4 passing
Aspire CLI build-ownership tests: 7 passing

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No

Copilot AI balanced review requested due to automatic review settings August 7, 2026 14:40
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🚀 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/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19132

Or

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Versions the CLI–VS Code AppHost build-ownership handshake to prevent stale launches.

Changes:

  • Introduces the exact .v2 capability contract on both sides.
  • Adds CLI, extension unit, and E2E regression coverage.
  • Wires the new E2E test into CI; the brace-glob spec currently breaks the existing Test-Path validation.
Show a summary per file
File Description
src/Aspire.Cli/Utils/ExtensionHelper.cs Advertises the v2 capability.
src/Aspire.Cli/Projects/DotNetAppHostProject.cs Requires v2 before CLI build ownership.
extension/src/capabilities.ts Advertises and centralizes the v2 token.
extension/src/server/interactionService.ts Requires v2 before skipping extension builds.
tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs Tests build ownership across launch modes and versions.
tests/Aspire.Cli.Tests/Commands/ConfigCommandTests.cs Verifies only v2 is advertised.
extension/src/test/capabilities.test.ts Tests extension capability advertisement.
extension/src/test/rpc/interactionServiceTests.test.ts Tests CLI compatibility behavior.
extension/src/test-e2e/buildOwnership.e2e.test.ts Adds stale-output regression coverage.
extension/CONTRIBUTING.md Documents the expanded E2E shard command.
.github/workflows/extension-e2e-tests.yml Adds the regression test to Linux and Windows shards.

Review details

Suppressed comments (1)

.github/workflows/extension-e2e-tests.yml:176

  • This spec never reaches the E2E runner: the existing compile guard at lines 421-422 calls PowerShell Test-Path with matrix.spec, and PowerShell does not expand {debugDashboard,buildOwnership} brace alternatives. It therefore checks for a literal brace-named file and fails the Windows shard. Please make that guard validate glob matches (using the runner's matcher) or split these into individually valid matrix entries.
            spec: 'out/test-e2e/test-e2e/{debugDashboard,buildOwnership}.e2e.test.js'
  • Files reviewed: 11/11 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread .github/workflows/extension-e2e-tests.yml Outdated
Comment thread extension/src/capabilities.ts Outdated
Comment thread src/Aspire.Cli/Utils/ExtensionHelper.cs Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Adam Ratzman (adamint) pushed a commit to adamint/aspire that referenced this pull request Aug 7, 2026
…dshake

Review feedback on microsoft#19132.

The buildOwnership E2E spec was folded into the debug-dashboard shard as
'{debugDashboard,buildOwnership}.e2e.test.js'. scripts/run-e2e.js expands brace
alternation, but the workflow's compile guard does not: it calls PowerShell
Test-Path, which only understands literal paths and the *, ? and [] wildcards.
Verified locally -- Test-Path on the brace form returns False while the literal
path returns True -- so the guard would have thrown "Compiled E2E shard spec was
not found" and failed both debug-dashboard shards before the spec ever ran.

Split it into its own build-ownership shard (Linux and Windows) instead of
teaching the guard to glob. Every other matrix entry is one literal spec per
shard, so this keeps the matrix uniform, restores debug-dashboard to what it
was, and gives the new test its own runner, timeout budget and diagnostics
artifact rather than extending an already long UI shard. A comment on the guard
records why matrix specs must stay literal. CONTRIBUTING lists the new shard;
its glob examples stay, since those run through run-e2e.js and not the guard.

Also rewrote the capability comments on both sides. They claimed "whichever side
advertises this promises the AppHost is built exactly once", which reads as a
symmetric promise and contradicts BuildAppHostIfNeededAsync. The contract is
asymmetric: the CLI advertising v2 is a promise ("I pre-build before every
launch"), which is why InteractionService passes forceBuild: false, while an
extension advertising v2 is a request ("you own the pre-build"), which is why
the CLI builds only when it sees the token. Each comment now states its own
side's meaning first, and both explain why the unversioned token could not carry
the CLI-side promise.

No behavior change; comments, workflow matrix and docs only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 15:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (2)

extension/src/capabilities.ts:40

  • The “built exactly once” guarantee does not hold for project-based AppHosts with an Executable launch profile. In extension/src/debugger/languages/dotnet.ts:471-482, shouldBuildProject is always true for non-file-based projects, so the extension calls buildDotNetProject even when v2 made forceBuild false, after the CLI has already pre-built. Since duplicate builds are explicitly acceptable here, document the actual no-skipped-build guarantee rather than an invariant the implementation does not provide.
 * Either way the AppHost is built exactly once, and neither side has to know the other's version -
 * only whether it speaks this contract.

src/Aspire.Cli/Utils/ExtensionHelper.cs:48

  • The “built exactly once” guarantee does not hold for project-based AppHosts with an Executable launch profile. In extension/src/debugger/languages/dotnet.ts:471-482, the extension still calls buildDotNetProject when forceBuild is false, after this CLI has already pre-built. Please state the actual no-skipped-build guarantee here instead of promising exactly one build.
    // Either way the AppHost is built exactly once, and neither side has to know the other's
    // version -- only whether it speaks this contract.
  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings August 7, 2026 15:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (1)

extension/src/server/interactionService.ts:650

  • forceBuild: false does not actually transfer build ownership for a non-file AppHost that selects an Executable launch profile. In extension/src/debugger/languages/dotnet.ts:418, shouldBuildProject is always true when !isFileBasedProject, and lines 480-482 then build unconditionally. A new CLI/new extension therefore builds twice on this supported path, contradicting the v2 contract's “extension skips its own build” guarantee. Make the Executable branch honor forceBuild === false for AppHosts and add the corresponding debugger test.
        const cliBuiltProject = cliCapabilities.includes(buildDotnetUsingCliCapability);
        const forceBuild = !cliBuiltProject;

        return debugSession.startAppHost(projectFile, args, environment, debug, { forceBuild });
  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI review requested due to automatic review settings August 7, 2026 17:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Adam Ratzman and others added 3 commits August 7, 2026 16:15
…e output

The `build-dotnet-using-cli` capability was a plain token, not proof that the
CLI actually pre-built the AppHost. CLI 13.2.0-13.2.4 advertised it
unconditionally, but derived watch mode from `isExtensionHost &&
!StartDebugSession`, so a no-debug "Run with Aspire" launch turned watch on and
skipped the pre-build entirely. Meanwhile the CLI never spawns `dotnet watch` in
extension host mode -- it hands the launch to the extension over RPC -- so
nothing rebuilt. An extension that trusted the token skipped its own build too,
and the user silently ran stale output. That is the original microsoft#15850 symptom.

Rename the capability to `build-dotnet-using-cli.v2` on both sides. Build
ownership now transfers only when the peer understands the newer contract, which
guarantees a pre-build on every launch (debug and no-debug). Matching is exact,
not prefix-based, so a future revision has to opt in deliberately.

Compatibility (the AppHost is built exactly once in every combination):

| CLI       | Extension | CLI pre-builds | Extension builds |
|-----------|-----------|----------------|------------------|
| <= 13.2.4 | <= 13.2.4 | debug only     | no               |
| <= 13.2.4 | new       | no             | yes              |
| new       | <= 13.2.4 | no             | yes              |
| new       | new       | yes            | no               |

Only the already-shipped old/old pair can still run stale output on the no-debug
path; nothing either new side ships can influence that combination. Every
combination involving new bits builds exactly once.

Tests: the extension unit test asserts that a CLI advertising the legacy
unversioned token still forces an extension build, and the CLI theory covers
legacy/v2/v3 tokens across both debug and no-debug launches while asserting
watch mode stays off. A new E2E test edits a running AppHost, relaunches, and
requires the compiler error to surface instead of stale output.

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

Review feedback on microsoft#19132.

The buildOwnership E2E spec was folded into the debug-dashboard shard as
'{debugDashboard,buildOwnership}.e2e.test.js'. scripts/run-e2e.js expands brace
alternation, but the workflow's compile guard does not: it calls PowerShell
Test-Path, which only understands literal paths and the *, ? and [] wildcards.
Verified locally -- Test-Path on the brace form returns False while the literal
path returns True -- so the guard would have thrown "Compiled E2E shard spec was
not found" and failed both debug-dashboard shards before the spec ever ran.

Split it into its own build-ownership shard (Linux and Windows) instead of
teaching the guard to glob. Every other matrix entry is one literal spec per
shard, so this keeps the matrix uniform, restores debug-dashboard to what it
was, and gives the new test its own runner, timeout budget and diagnostics
artifact rather than extending an already long UI shard. A comment on the guard
records why matrix specs must stay literal. CONTRIBUTING lists the new shard;
its glob examples stay, since those run through run-e2e.js and not the guard.

Also rewrote the capability comments on both sides. They claimed "whichever side
advertises this promises the AppHost is built exactly once", which reads as a
symmetric promise and contradicts BuildAppHostIfNeededAsync. The contract is
asymmetric: the CLI advertising v2 is a promise ("I pre-build before every
launch"), which is why InteractionService passes forceBuild: false, while an
extension advertising v2 is a request ("you own the pre-build"), which is why
the CLI builds only when it sees the token. Each comment now states its own
side's meaning first, and both explain why the unversioned token could not carry
the CLI-side promise.

No behavior change; comments, workflow matrix and docs only.

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

The comments I added in 9621821 claimed "the AppHost is built exactly once".
That is not true on every path. In extension/src/debugger/languages/dotnet.ts
shouldBuildProject is `!isFileBasedProject || !isApphost || forceBuild !== false`,
so for a project-based AppHost the first term is already true and the Executable
launch-profile branch calls buildDotNetProject unconditionally -- even when the
CLI advertised v2 and InteractionService passed forceBuild: false. A new CLI
paired with a new extension therefore builds twice on that path.

Leaving the wording alone would have documented an invariant the implementation
does not provide, which is the same class of mistake the version bump exists to
fix. State the guarantee that actually holds instead: neither side skips the
build believing the other did it, so no launch runs stale output. Redundant
builds are tolerated and the reason is recorded -- that build compiles the
launch profile's dependencies rather than the AppHost output, so a wasted
incremental build is acceptable where a skipped one is not.

Comment-only; no behavior change. Making the Executable branch honor
forceBuild === false is a real behavior change to class-library integration
launches and does not belong in this capability-versioning PR.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 16/16 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

The build-ownership E2E shard timed out at 180s waiting for a running
AppHost on both Linux and Windows even though the debug console showed
the AppHost reaching "Distributed application started".

waitForWorkspaceAppHost() opens the E2E workspace folder on the first
spec of a shard, which reloads the VS Code window back to the Explorer
view. AppHostDataRepository only polls `aspire ps` while the Aspire
panel is visible or an AppHost tab is open (its `_dataActive` gate), and
`aspire ps` is the only source of `state.appHosts` -- the list every
running-AppHost assertion reads. The captured extension log confirms it:
`aspire ps --follow` is spawned in the pre-reload activation and never
again in the post-reload workspace activation, and the failure
screenshot shows the Explorer sidebar with the Aspire view unselected.

Every other spec that waits for a running AppHost already reopens the
view after the folder open; this one did not. Add the missing
openAspireView() and a unit-level guard that scans the spec sources so
the next spec cannot reintroduce the same hang.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 10, 2026 01:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 16/16 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI review requested due to automatic review settings August 10, 2026 07:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 16/16 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

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

Copilot-Session: 0d1cf760-b1bc-46ba-a6d4-628354b00f2c
Copilot AI review requested due to automatic review settings August 10, 2026 15:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (1)

extension/src/server/interactionService.ts:647

  • Mandatory: This cross-process regression still lacks VS Code E2E coverage. The new unit tests stub getCliCapabilities() and startAppHost() separately, so they cannot catch a mismatch in the real capability transport or the no-debug “Run with Aspire” launch—the integration that originally allowed stale output. Add an extension E2E test that launches without debugging, changes or breaks the AppHost source, relaunches, and verifies that the new build runs or its compiler failure is surfaced; existing E2E coverage only verifies command routing (extension/src/test-e2e/packageSurface.e2e.test.ts:225-265).
        const cliBuiltProject = cliCapabilities.includes(buildDotnetUsingCliCapability);
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VS Code: Run with aspire doesn't rebuilt app host if out of date

2 participants