Skip to content

Fail on JavaScript run script Dockerfile conflict#16969

Merged
mitchdenny merged 7 commits into
mainfrom
sebastienros/sebros-fix-javascriptapp-entrypoint
May 19, 2026
Merged

Fail on JavaScript run script Dockerfile conflict#16969
mitchdenny merged 7 commits into
mainfrom
sebastienros/sebros-fix-javascriptapp-entrypoint

Conversation

@sebastienros
Copy link
Copy Markdown
Contributor

Description

When AddJavaScriptApp is published with an existing Dockerfile, an explicit runScriptName or later WithRunScript(...) can be silently ignored because the Dockerfile entrypoint wins. This change fails fast with a clear error when Aspire cannot safely apply the requested run script to a user-authored Dockerfile.

The validation is applied to both the original JavaScript executable resource and the replacement container resource used during full manifest publishing. It still allows the implicit default run script and explicit PublishAsDockerFile(...) container entrypoint overrides.

Fixes: #16957

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

Detect explicit JavaScript run script configuration during publish when an existing Dockerfile would otherwise supply the container entrypoint. Fail with a clear message instead of silently ignoring runScriptName or WithRunScript, while preserving explicit container entrypoint overrides.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 12, 2026 00:24
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 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/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16969

Or

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

@sebastienros
Copy link
Copy Markdown
Contributor Author

@PatrickMatthiesen would appreciate if you can try the dogfood build once it's available to confirm this is fixing the issue you filed.

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 PR fixes a publish-time pitfall for AddJavaScriptApp when an existing user-authored Dockerfile is discovered: if the app is configured with an explicit runScriptName / WithRunScript(...), Aspire now fails fast during manifest publishing rather than silently emitting a container that will run the Dockerfile ENTRYPOINT instead of the requested script.

Changes:

  • Introduces publish-time validation that detects “existing Dockerfile + explicit run script” conflicts and throws a clear InvalidOperationException.
  • Applies the validation consistently when publishing an individual JavaScript resource manifest and when publishing the full model manifest.
  • Adds unit tests covering the new failure cases and the allowed cases (implicit default script, explicit entrypoint override via PublishAsDockerFile).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs Adds validation hooks during manifest publishing to block explicit run-script configuration when publishing uses an existing Dockerfile entrypoint.
tests/Aspire.Hosting.JavaScript.Tests/AddJavaScriptAppTests.cs Adds tests verifying the new failure mode and the scenarios that remain permitted.

@davidfowl
Copy link
Copy Markdown
Contributor

Make sure deployment failures happen in steps not during model building

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
private const string BrowserCapability = "browser";
private const string DefaultNodeVersion = "22";
private const string DefaultJavaScriptRunScriptName = "dev";
private const string PublishManifestStepName = "publish-manifest";
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.

Before start? @eerhardt ?

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

Oh, nice, thanks for giving it a look so quickly!

It sadly didn't fix the issue, or maybe I used the wrong dogfood build?

I made a small reproducible example repo, if you need more info or want a way to debug it :)

https://github.com/PatrickMatthiesen/dockerfile-aspire-bug

@sebastienros
Copy link
Copy Markdown
Contributor Author

sebastienros commented May 12, 2026

Check aspire --version, at least it should show this PR number in it. I tried your repository and it works as expected with this PR, and fails without it.

@PatrickMatthiesen
Copy link
Copy Markdown

PatrickMatthiesen commented May 12, 2026

That seems weird, as the version is reported as being this PR's Dogfood for me:

aspire --version
13.4.0-pr.16969.g02cec45b

Anything else I should try checking?

Btw, did you see the README? I shipped the repo in a working state so you could see that removing either the Dockerfile entrypoint overwrite or the PublishAsDockerFile(...) entirely, breaks the deploy.

If I do either of the two things, then the migrations resource fails, and the observer resource never starts:

const applyMigrations = builder.addJavaScriptApp('apply-migrations', '..', {
    runScriptName: 'migrate:postgres-schema'
}).withBun()
    .publishAsDockerFile(async (container) => {
        // await container.withEntrypoint('bun');
        // await container.withArgs(['migrate:postgres-schema']);
    });
image image

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
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.

Reviewed the implementation and reproduced two UX issues by running the PR build against a small AppHost (aspire publish --publisher manifest). Two inline comments below.

The overall fix works: explicit runScriptName with an existing user-authored Dockerfile correctly fails publish with a clear exception, and the escape hatch (PublishAsDockerFile(c => c.WithEntrypoint(...))) is honored.

var hasExplicitRunScript =
!string.Equals(initialRunScriptName, DefaultJavaScriptRunScriptName, StringComparison.Ordinal) ||
hasMultipleRunScriptAnnotations ||
runScript.Args.Length > 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hasMultipleRunScriptAnnotations is true whenever WithRunScript(...) is called after AddJavaScriptApp, because CreateDefaultJavaScriptAppBuilder itself already adds one annotation at line 876. As a result, this code throws even when the user's explicit WithRunScript matches the default:

builder.AddJavaScriptApp("js", appDir)
       .WithBun()
       .WithRunScript("dev"); // same as the default

Repro (verified against this PR build with dotnet run -- --publisher manifest): publish fails with JavaScript app resource 'js' is configured to run script 'dev', but publish is using the existing Dockerfile .... The user explicitly affirmed the default value, so the failure is surprising.

Suggested fix: compare the last JavaScriptRunScriptAnnotation's ScriptName (and Args) against the default instead of treating annotation count alone as a proxy for "explicit".

throw new DistributedApplicationException(
$"JavaScript app resource '{resource.Name}' is configured to run script '{runScript.ScriptName}', but publish is using the existing Dockerfile '{dockerfileBuildAnnotation.DockerfilePath}'. " +
"An existing Dockerfile entrypoint cannot be changed automatically from runScriptName or WithRunScript. " +
"Remove or rename the Dockerfile so Aspire can generate one, or call PublishAsDockerFile(...) and set the container entrypoint explicitly.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hasExplicitRunScript can be set purely by runScript.Args.Length > 0, even when runScript.ScriptName equals the default "dev". But the thrown message only mentions runScriptName and WithRunScript (the script name), not the args. So this:

builder.AddJavaScriptApp("js", appDir) // default script: "dev"
       .WithBun()
       .WithRunScript("dev", new[] { "--port", "8080" });

fails with ... is configured to run script 'dev', but publish is using the existing Dockerfile ... — no mention of the args, which are the actual reason for the failure. Verified locally: the message produced for the args-only case is identical to the case where the user passed a non-default script name.

Suggested fix: when the trigger is runScript.Args.Length > 0, include the args in the message (e.g. ... is configured to run script 'dev' with args [--port, 8080], ...).

@IEvangelist
Copy link
Copy Markdown
Member

Tested this PR with the dogfood CLI 13.4.0-pr.16969.gd182e0c9 (matches head d182e0c9) via aspire publish against a JavaScript app resource. Ran four scenarios:

Scenario Setup Expected Result
S1 WithRunScript("migrate") + existing Dockerfile (no PublishAsDockerFile) Fail fast with clear error ✅ exit 6 — error message fires, names the rule, the resource, the Dockerfile path, and the two fixes
S2 WithRunScript + PublishAsDockerFile(c => c.WithEntrypoint("node").WithArgs(...)) Succeed (user took control of entrypoint) ✅ exit 0
S3 No explicit run script (implicit default) + Dockerfile Succeed (no conflict) ✅ exit 0
S4 WithRunScript only, no Dockerfile (Aspire generates one) Succeed ✅ exit 0

Error text from S1 is good — it explains why it failed and gives two unambiguous fixes (remove/rename the Dockerfile, or call PublishAsDockerFile and set entrypoint explicitly). 👍

Code review: nothing blocking. (One reviewer flagged the two WithManifestPublishingCallback registrations on line 889–890 — verified those are intentional: resourceBuilder wraps the JavaScriptAppResource and containerBuilder wraps the replacement ContainerResource, so they're registering against different resources.)

cc @sebastienros

@davidfowl
Copy link
Copy Markdown
Contributor

This needs a pr-testing report.

Mitch Denny and others added 2 commits May 19, 2026 15:57
- Detect 'explicit run script' by inspecting the last
  JavaScriptRunScriptAnnotation's ScriptName and Args rather than
  counting annotations. This avoids a false positive when the user
  re-states the default explicitly (e.g. .WithRunScript("dev")).
- When the run-script args are the trigger for the conflict, include
  them in the thrown message so users can see why a default-named
  script still produced a conflict.

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

# Conflicts:
#	tests/Aspire.Hosting.JavaScript.Tests/AddJavaScriptAppTests.cs
@mitchdenny mitchdenny enabled auto-merge (squash) May 19, 2026 06:07
Copy link
Copy Markdown
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.

LGTM — patched the two issues I flagged earlier (overly aggressive heuristic + missing args context in error message) directly on this branch, all 137 JS hosting tests pass locally, and CI is green for everything related to this change.

The only red job (Azure.Messaging.EventHubs / windows-latest) is an unrelated infra flake: failed: 0 across all TFMs with a hang-dump detection at the job level. Nothing in this PR touches Azure or EventHubs.

@davidfowl
Copy link
Copy Markdown
Contributor

Lets verify #16969 (comment)

@sebastienros
Copy link
Copy Markdown
Contributor Author

Before we continue on this PR, I asked Eric to verify and he commented on the issue directly that it's the documented behavior: #16957 (comment)

@IEvangelist
Copy link
Copy Markdown
Member

Independent repro: PatrickMatthiesen/dockerfile-aspire-bug ✅

I cloned PatrickMatthiesen/dockerfile-aspire-bug and ran his README repro against this PR's CLI build. The PR catches the silent-failure scenario cleanly.

Setup

  • PR CLI installed: 13.4.0-pr.16969.g64ea7196 (matches PR head 64ea7196) ✅
  • Host: Windows, Docker 29.4.3, Node v24.13.1
  • Project: PatrickMatthiesen's repro, with the root Dockerfile (ENTRYPOINT ["bun", "src/index.ts"]) and an apphost.ts declaring apply-migrations and observer JavaScript apps. The root Dockerfile is the source of the conflict — its entrypoint silently won on apply-migrations previously.

Test 1 — README Step 1 (working state, both apps override entrypoint via .publishAsDockerFile(...))

aspire publish --non-interactive

  • Exit 0, 9/9 steps succeeded.
  • Both new validation steps validate-javascript-dockerfile-run-script-apply-migrations and ...-observer fired and succeeded. ✅

Test 2 — README Step 2 (bug state, .publishAsDockerFile(...) removed from apply-migrations only)

const applyMigrations = builder.addJavaScriptApp('apply-migrations', '..', {
    runScriptName: 'migrate:postgres-schema'
}).withBun();  // ← no .publishAsDockerFile(...)

aspire publish --non-interactiveExit 6, pipeline failed with:

(validate-javascript-dockerfile-run-script-apply-migrations) ✗ Step failed.
✗ JavaScript app resource 'apply-migrations' is configured to run script
  'migrate:postgres-schema', but publish is using the existing Dockerfile
  'C:\...\repro\Dockerfile'. An existing Dockerfile entrypoint cannot be
  changed automatically from runScriptName or WithRunScript. Remove or
  rename the Dockerfile so Aspire can generate one, or call
  PublishAsDockerFile(...) and set the container entrypoint explicitly.

Notably, validate-javascript-dockerfile-run-script-observer still passed in the same run — the per-app step targets only the offending resource. 👍

Test 3 — same bug state via aspire deploy --non-interactive

Same failure, same step name, same error message — confirming validation is wired into the pipeline (not just publish-time manifest emission), so deploys are protected too. ✅

Findings

  1. The error message is actionable: it names the resource, the script, the conflicting Dockerfile path, and gives two clear remediations.
  2. Validation is per-resource — a misconfigured app does not mask correctly-configured siblings.
  3. Validation fires on both publish and deploy, matching the decoupling from manifest generation in d182e0c9.
  4. Net effect for the original repro: instead of apply-migrations silently starting bun src/index.ts (the wrong entrypoint) and the user puzzling over why migrations never ran, they now get a fail-fast error pointing directly at the fix.

LGTM from a behavioral standpoint. 🚀

Tested by @david-pine using the pr-testing skill against PR head 64ea7196.

@mitchdenny mitchdenny merged commit 7299c94 into main May 19, 2026
596 of 599 checks passed
@microsoft-github-policy-service microsoft-github-policy-service Bot added this to the 13.4 milestone May 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 92 passed, 0 failed, 2 unknown (commit 64ea719)

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View recording
AddPackageWhileAppHostRunningDetached ▶️ View recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View recording
AgentInitCommand_DefaultSelection_InstallsDefaultSkills ▶️ View recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View recording
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost ▶️ View recording
AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAndPreservesFiles ▶️ View recording
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive ▶️ View recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View recording
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent ▶️ View recording
Banner_DisplayedOnFirstRun ▶️ View recording
Banner_DisplayedWithExplicitFlag ▶️ View recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View recording
CertificatesClean_RemovesCertificates ▶️ View recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View recording
CreateAndRunAspireStarterProject ▶️ View recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View recording
CreateAndRunEmptyAppHostProject ▶️ View recording
CreateAndRunJavaEmptyAppHostProject ▶️ View recording
CreateAndRunJsReactProject ▶️ View recording
CreateAndRunPythonReactProject ▶️ View recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View recording
CreateAndRunTypeScriptStarterProject ▶️ View recording
CreateJavaAppHostWithViteApp ▶️ View recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View recording
DeployK8sBasicApiService ▶️ View recording
DeployK8sWithExternalHelmChart ▶️ View recording
DeployK8sWithGarnet ▶️ View recording
DeployK8sWithMongoDB ▶️ View recording
DeployK8sWithMySql ▶️ View recording
DeployK8sWithPostgres ▶️ View recording
DeployK8sWithRabbitMQ ▶️ View recording
DeployK8sWithRedis ▶️ View recording
DeployK8sWithSqlServer ▶️ View recording
DeployK8sWithValkey ▶️ View recording
DeployTypeScriptAppToKubernetes ▶️ View recording
DescribeCommandResolvesReplicaNames ▶️ View recording
DescribeCommandShowsRunningResources ▶️ View recording
DetachFormatJsonProducesValidJson ▶️ View recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View recording
DoListStepsShowsPipelineSteps ▶️ View recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View recording
GlobalMigration_PreservesAllValueTypes ▶️ View recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View recording
InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot ▶️ View recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View recording
LatestCliCanStartStableChannelAppHost ▶️ View recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View recording
LogLevelTrace_ProducesTraceEntriesInCliLogFile ▶️ View recording
LogsCommandShowsResourceLogs ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterApp ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterAppIsolated ▶️ View recording
PsCommandListsRunningAppHost ▶️ View recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View recording
ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogContainsEntries ▶️ View recording
ResourceCommand_FailsWhenInteractionServiceIsRequired ▶️ View recording
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput ▶️ View recording
RestoreGeneratesSdkFiles ▶️ View recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View recording
RunPublishFailureScenarioAsync ▶️ View recording
RunReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
RunReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
SecretCrudOnDotNetAppHost ▶️ View recording
SecretCrudOnTypeScriptAppHost ▶️ View recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View recording
StartReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
StartReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
StopAllAppHostsFromAppHostDirectory ▶️ View recording
StopJavaPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopNonInteractiveSingleAppHost ▶️ View recording
StopTypeScriptPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View recording
UpdateProjectChannelToStable_TypeScript_PicksUpStablePackages ▶️ View recording

📹 Recordings uploaded automatically from CI run #26079523876

aspire-repo-bot Bot added a commit to microsoft/aspire.dev that referenced this pull request May 19, 2026
When AddJavaScriptApp is configured with an explicit runScriptName or
WithRunScript and an existing user-authored Dockerfile is present,
Aspire now fails at publish time with a clear error instead of silently
ignoring the requested run script.

Document the conflict, the error message, and the two resolution paths:
remove/rename the Dockerfile, or use PublishAsDockerFile with an
explicit container entrypoint.

Documents changes from microsoft/aspire#16969.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@aspire-repo-bot
Copy link
Copy Markdown
Contributor

Pull request created: #1014

Generated by PR Documentation Check

@aspire-repo-bot
Copy link
Copy Markdown
Contributor

📝 Documentation has been drafted in microsoft/aspire.dev#1014 targeting release/13.4.

Added a new subsection "Run script and existing Dockerfile conflict" to deployment/javascript-apps.mdx documenting the new publish-time validation introduced in this PR. The subsection explains when the conflict occurs (explicit runScriptName or .WithRunScript(...) combined with an existing user-authored Dockerfile), shows the exact error message thrown, and documents the two resolution paths: removing/renaming the Dockerfile, or using PublishAsDockerFile(...) with an explicit container entrypoint. A new bullet was also added to the "Common mistakes" section.

  • Modified: src/frontend/src/content/docs/deployment/javascript-apps.mdx

Note

This draft PR needs human review before merging.

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.

TypeScript AddJavaScriptApp silently reuses discovered Dockerfile entrypoint and ignores runScriptName / withRunScript

6 participants