Skip to content

Defer JavaScript installer when the app uses WithExplicitStart - #21504

Open
David Fowler (davidfowl) wants to merge 2 commits into
mainfrom
fix/js-installer-explicit-start-20186
Open

David Fowler (davidfowl) wants to merge 2 commits into
mainfrom
fix/js-installer-explicit-start-20186

Conversation

@davidfowl

@davidfowl David Fowler (davidfowl) commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Description

WithNpm(install: true) (likewise Yarn, pnpm, Bun and Deno) creates a <app>-installer resource, and the app waits for it to complete. If the app also uses WithExplicitStart(), the installer ignored that. It still started automatically during AppHost startup and ran npm install, even though the app itself was deferred.

Root cause

In JavaScriptHostingExtensions (AddInstaller), the generated JavaScriptInstallerResource only gets an ExplicitStartupAnnotation for install: false. Nothing looks at the parent's ExplicitStartupAnnotation. Simply copying the annotation when the installer is created isn't enough, for two reasons:

  • WithExplicitStart() is commonly called after WithNpm(...), as in the issue's repro.
  • A deferred installer must still run when the user starts the app. Otherwise the app's WaitForCompletion(installer) never completes, because no orchestrator mechanism starts explicit-start dependencies.

Fix

  • In the installer's existing OnBeforeStart callback, which runs after all builder calls, the installer is marked explicit-start when the parent is explicit-start and waits for the installer (install: true).
  • A BeforeResourceStartedEvent subscription on the parent starts the deferred installer through the public start command (ResourceCommandService), but only if the installer is still NotStarted. Resources are initialized concurrently, so the handler first waits for the installer to publish its initial state rather than skipping it. A semaphore serializes the check and start across replicas. When the app is restarted, an installer that has already finished is not rerun. Builder-time subscriptions run before the orchestrator's WaitFor handler, which is registered when the app starts, so the installer is started before the app begins waiting on it.
  • install: false and non-explicit apps keep their existing behavior.

Repro / before-after evidence

New tests in PackageInstallationTests:

Test main (13b2068) this PR
InstallerIsDeferredWhenParentUsesExplicitStart(explicitStartBeforePackageManager: True) ❌ Assert.True() Failure: installer has no ExplicitStartupAnnotation ✅
InstallerIsDeferredWhenParentUsesExplicitStart(explicitStartBeforePackageManager: False) (issue's exact repro order) ❌ Assert.True() Failure ✅
StartingExplicitStartParentStartsDeferredInstallerOnce ❌ Assert.Equal() Failure (expected 1 installer start, got 0) ✅
StartingParentBeforeInstallerStateIsPublishedStillStartsInstaller (startup ordering race from review) ❌ Assert.False() Failure (also fails on this PR's first head, 6789435) ✅
ExplicitStartInstallerFunctionalTests.ExplicitStartAppDefersInstallerUntilAppIsStarted (real DCP + npm: the installer stays NotStarted, runs once when the app starts, and is not rerun on app restart) ❌ Expected: "NotStarted" Actual: "Starting" (the reported bug) ✅
InstallerIsNotDeferredWhenParentStartsAutomatically ✅ ✅ (existing behavior preserved)
StartingParentDoesNotStartInstallerDisabledWithInstallFalse ✅ ✅ (existing behavior preserved)

E2E evidence (real DCP + npm)

ExplicitStartInstallerFunctionalTests.ExplicitStartAppDefersInstallerUntilAppIsStarted runs a real AppHost under DCP with a real npm project. The test:

  1. Checks that the installer starts as NotStarted and that the postinstall marker file doesn't exist.
  2. Starts the app through ResourceCommandService. The installer exits 0, and the app reaches Running and serves HTTP.
  3. Stops and restarts the app, and checks that the installer did not run again (the marker file is unchanged).

On main this test fails with Expected: "NotStarted" Actual: "Starting", which is the bug reported in #20186. With this PR it passes locally and in CI on head ceaf0f2: the Hosting.JavaScript jobs on ubuntu-latest and windows-latest each passed 426/426 with 0 skipped.

Local results:

  • PackageInstallationTests + ExplicitStartInstallerFunctionalTests + NodeFunctionalTests: 33/33 passed. On main, 5 of these 31 targeted tests fail.
  • Full Aspire.Hosting.JavaScript.Tests: 410 of 429 passed. The 19 local failures are all environmental and also unaffected by this run-mode-only change:
    • Dockerfile snapshot tests running as root, where /root/ is scrubbed to {UserProfile}. These run in publish mode.
    • Bun and Deno functional tests, because those runtimes aren't installed locally.

Fixes #20186

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
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

🤖 Posted by AspireAgent (AI Agent)

When a JavaScript app is configured with WithExplicitStart() and its package
manager installs dependencies (install: true), the generated installer resource
started automatically during AppHost startup. The installer now inherits the
app's explicit start, and starting the app starts the deferred installer once so
the app's WaitForCompletion dependency is satisfied.

Fixes #20186

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

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 -- 21504

Or

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

@aspire-repo-bot
aspire-repo-bot Bot requested a balanced review from Copilot October 30, 2026 22:53
@github-actions github-actions Bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Oct 30, 2026
@github-actions

This comment has been minimized.

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.

🟡 Changes recommended

A startup race can skip the installer and leave the parent blocked indefinitely, while the current tests bypass the runtime ordering needed to detect it.

2 open issues
What changed in this PR

This PR addresses #20186 by deferring JavaScript package installers alongside apps configured with WithExplicitStart(), then starting the installer when the parent app is explicitly started.

Changes:

  • Evaluate the parent’s explicit-start configuration after builder calls finish.
  • Start deferred installers on parent startup, with serialized state checks to avoid rerunning completed installers.
  • Add tests for configuration order, parent startup and restart, and preservation of automatic-start and install: false behavior.
File Description
tests/​Aspire.Hosting.JavaScript.Tests/​PackageInstallationTests.cs Adds installer deferral and parent-start regression tests.
src/​Aspire.Hosting.JavaScript/​JavaScriptHostingExtensions.cs Propagates explicit-start behavior and starts deferred installers with their parent.

Review effort: Balanced

Resources are initialized concurrently, so an explicit-start app can be started
before its installer publishes its initial state. Wait for that state instead of
skipping the installer, and add a deterministic regression test plus a real
DCP/npm functional test covering deferral, start-on-demand and restart.

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.

🟢 Approval recommended

No active review issues block the changes to installer startup behavior.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Tests selector

10 / 100 PR test projects · 4 PR jobs, from 3 changed files.

Selected PR test projects (10 / 100)

Aspire.Cli.EndToEnd.Tests, Aspire.Hosting.Azure.Kubernetes.Tests, Aspire.Hosting.Azure.Tests, Aspire.Hosting.Blazor.Tests, Aspire.Hosting.CodeGeneration.TypeScript.Tests, Aspire.Hosting.Dotnet.Tests, Aspire.Hosting.JavaScript.Tests, Aspire.Hosting.Radius.Tests, Aspire.Hosting.Tests, Aspire.Playground.Tests

Selected PR jobs (4)

cli-starter-validation, extension-e2e, polyglot, typescript-api-compat


How these were chosen — grouped by what changed

⚠️ 9 of the 10 selected test projects come from a single change — src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs.

🔧 src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs (changed source)
→ 1 directly: Aspire.Hosting.JavaScript.Tests
→ 8 via the project graph: Aspire.Hosting.Azure.Kubernetes.Tests (2 hops), Aspire.Hosting.Azure.Tests, Aspire.Hosting.Blazor.Tests (2 hops), Aspire.Hosting.CodeGeneration.TypeScript.Tests, Aspire.Hosting.Dotnet.Tests (2 hops), Aspire.Hosting.Radius.Tests (2 hops), Aspire.Hosting.Tests, Aspire.Playground.Tests

📦 affected project Aspire.Hosting.JavaScript
→ 1 test: Aspire.Cli.EndToEnd.Tests

🧪 tests/Aspire.Hosting.JavaScript.Tests/ExplicitStartInstallerFunctionalTests.cs (changed test)
→ 1 directly: Aspire.Hosting.JavaScript.Tests

🧪 tests/Aspire.Hosting.JavaScript.Tests/PackageInstallationTests.cs (changed test)
→ 1 directly: Aspire.Hosting.JavaScript.Tests

Job reasons

Job Triggered by
cli-starter-validation affected project Aspire.Hosting.JavaScript
extension-e2e • src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs
• affected project Aspire.Hosting.JavaScript
polyglot affected project Aspire.Hosting.JavaScript
typescript-api-compat affected project Aspire.Hosting.JavaScript

Selection computed for commit ceaf0f2.

Copy link
Copy Markdown
Collaborator Author

CI notes for head ceaf0f2 (run 41146506214). All JavaScript and Hosting jobs passed on attempt 1. The only failures were in jobs this change doesn't touch:

  • Cli.EndToEnd TypeScriptCodegenValidationTests (ubuntu), attempt 1: UnAwaitedChainsCompileWithAutoResolvePromises failed with "aspire start failed". Its apphost uses only Postgres and nginx, with no JavaScript installer. The job passed on rerun (attempt 2).
  • VS Code extension E2E (Windows, tree-actions):
    • Attempt 1: hosted runner lost communication (infra).
    • Attempt 2: the test passed, but teardown failed with EBUSY: resource busy or locked, rmdir ...\AspireE2E.SecondaryActions. This job also failed on main in run 41136039061.
    • Attempt 3: hosted runner lost communication again.
    • A final rerun (attempt 4) is in progress.

The tree-actions spec uses a single-file .NET AppHost and never reaches Aspire.Hosting.JavaScript.

🤖 Posted by AspireAgent (AI Agent)

This branch has not been deployed

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

Labels

needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JavaScript installer ignores parent WithExplicitStart()

2 participants