[release/13.3] Fix #15986: emit apphost.run.json from aspire init single-file skeleton#16821
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16821Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16821" |
There was a problem hiding this comment.
Pull request overview
Backports the fix for #15986 to release/13.3 by ensuring the C# single-file aspire init skeleton emits apphost.run.json, enabling the .NET file-based runner (dotnet run apphost.cs) to start successfully with the required dashboard/OTLP/resource-service launch profile environment variables.
Changes:
- Generate a single set of ports and use them to keep
aspire.config.jsonandapphost.run.jsonin sync. - Add
apphost.run.jsonemission for the single-file C# init path, including a best-effort adoption of ports from an existingaspire.config.jsonprofiles section when it matches the expected shape. - Add unit tests and a CLI E2E regression test covering
apphost.run.jsoncreation and basic schema/behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/Aspire.Cli/Commands/InitCommand.cs | Writes apphost.run.json for the single-file C# init skeleton and keeps ports aligned with aspire.config.json. |
| tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs | Adds unit coverage for apphost.run.json creation, port adoption, and preservation of unparseable existing profiles. |
| tests/Aspire.Cli.EndToEnd.Tests/SingleFileAppHostInitDotnetRunTests.cs | Adds an interactive E2E regression test verifying apphost.run.json is generated and dotnet run apphost.cs can start. |
| // Writes apphost.run.json next to the single-file AppHost so that | ||
| // `dotnet run apphost.cs` (.NET file-based runner) picks up the dashboard / OTLP / | ||
| // resource service launch profile env vars. Mirrors the structure shipped by the | ||
| // aspire-apphost-singlefile MSBuild template. Skips if the file already exists. | ||
| private void DropAppHostRunJson(DirectoryInfo directory, AppHostProfilePorts ports) |
| // If aspire.config.json already exists with a `profiles` section (e.g. user | ||
| // re-ran `aspire init` after editing it, or copied a stale file in), the new | ||
| // apphost.run.json must adopt those same ports — the two files should never | ||
| // disagree on dashboard / OTLP / resource service endpoints. |
| /// Before the fix, <c>aspire init</c> wrote <c>apphost.cs</c>, <c>aspire.config.json</c>, | ||
| /// and <c>NuGet.config</c> but skipped <c>apphost.run.json</c>. The .NET 10 file-based | ||
| /// runner only honours <c>[file].run.json</c> for launch profiles (it ignores | ||
| /// <c>aspire.config.json</c>), so without it <c>dotnet run apphost.cs</c> crashed at | ||
| /// startup with <c>OptionsValidationException</c> complaining that |
| // Generate one set of ports so aspire.config.json (used by `aspire run`) and | ||
| // apphost.run.json (used by `dotnet run apphost.cs`) agree on the dashboard / | ||
| // OTLP / resource service endpoints. | ||
| var ports = AppHostProfilePortGenerator.Generate(Random.Shared); | ||
|
|
…le apphost.cs in `aspire init` Cherry-picked into PR #16821 (backport of #16812) so that the new SingleFileAppHostInitDotnetRunTests E2E test can pass on release/13.3. Without this, `dotnet run apphost.cs` cannot resolve `Aspire.AppHost.Sdk@13.3.0` from the locally-built run-local hive because the workspace contains no NuGet.config — exactly the failure mode #16636 fixed on main. Stable end-users on the released 13.3.x are unaffected: CreateOrUpdateNuGetConfigWithoutPromptAsync no-ops for the default implicit/stable channel; only PR/local/dev/staging hives trigger a nuget.config drop. Conflict in src/Aspire.Cli/Commands/InitCommand.cs resolved by keeping all of #16812's port-generation + DropAspireConfig(effectivePorts) + DropAppHostRunJson flow intact and inserting the NuGet.config block between "Created apphost.cs" and the port generation. (cherry picked from commit 61dac51) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🎬 CLI E2E Test Recordings — 67 recordings uploaded (commit View all recordings
📹 Recordings uploaded automatically from CI run #25424761416 |
IEvangelist
left a comment
There was a problem hiding this comment.
Stale base / redundant cherry-pick — will not merge cleanly into release/13.3
This PR was opened against release/13.3 at base SHA 6d7482c1, but PR #16822 ("Drop nuget.config alongside single-file apphost.cs in aspire init (#16636) (#16822)") has since merged into release/13.3. PR #16822 is itself a backport of #16636 — the same PR that this branch also cherry-picks (commit 9b4bacb8).
git merge-tree --write-tree HEAD upstream/release/13.3 confirms a hard conflict:
CONFLICT (content): Merge conflict in src/Aspire.Cli/Commands/InitCommand.cs
Auto-merging src/Aspire.Cli/Templating/TemplateNuGetConfigService.cs
Recommended action: rebase this branch onto current release/13.3. The cherry-pick of #16636 (commit 9b4bacb8) should drop out automatically via git cherry patch-id matching (or be dropped manually), leaving only the apphost.run.json commit (0f277840) — which is the actual fix being backported here.
After the rebase, please double-check tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs doesn't end up with three services.AddSingleton<TemplateNuGetConfigService>() registrations (release/13.3 already has two at lines 117 and 148 after #16822 merged).
…on (#16812) * Add CLI E2E repro for #15986: dotnet run apphost.cs after aspire init Demonstrates that 'dotnet run apphost.cs' against the single-file C# AppHost dropped by interactive 'aspire init' fails because the launch profile env vars (ASPNETCORE_URLS, ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL, ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL) are missing — aspire init does not write apphost.run.json and the .NET file-based runner only honours that file (not aspire.config.json) for launch profiles. This commit intentionally adds a failing test; the fix follows in a subsequent commit on the same PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix #15986: emit apphost.run.json from aspire init single-file skeleton After 'aspire init' drops the C# single-file AppHost ('apphost.cs' + 'aspire.config.json' + 'NuGet.config'), running 'dotnet run apphost.cs' crashed at startup because no launch profile was applied: Failed to configure dashboard resource because ASPNETCORE_URLS environment variable was not set. Failed to configure dashboard resource because ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL and ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL are not set. The dashboard / OTLP / resource service env vars normally come from a launch profile. When the AppHost is launched via 'aspire run' the CLI injects them from 'aspire.config.json'. When launched via the .NET file-based runner ('dotnet run apphost.cs') only '<file>.run.json' (here 'apphost.run.json') is honoured, and the init flow was not producing it. Fix: in InitCommand.DropCSharpSingleFileSkeletonAsync, generate the profile ports once and pass them into both the existing 'aspire.config.json' writer and a new 'apphost.run.json' writer so the two files agree on the dashboard URLs. The 'apphost.run.json' shape mirrors the existing aspire-apphost-singlefile MSBuild template (commandName=Project, dotnetRunMessages=true, launchBrowser=true, ASPNETCORE_ENVIRONMENT/DOTNET_ENVIRONMENT=Development, dashboard / OTLP / resource service URLs). The aspire-apphost-singlefile MSBuild template is unaffected since it already ships 'apphost.run.json' alongside its other artifacts. No other call site of DropAspireConfig is changed (the new ports parameter is optional and defaults to the previous self-generation behaviour). Adds InitCommand_SingleFileSkeleton_CreatesAppHostRunJsonWithDashboardEnvVars unit test and updates the SingleFileAppHostInitDotnetRunTests E2E repro added in the previous commit. Fixes #15986 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add CLI E2E test for #15986: aspire init then dotnet run apphost.cs Drives the user-facing flow that #15986 broke: 1. `aspire init` (interactive, default C# selection). 2. Inspect the bind-mounted workspace from the host: assert `apphost.cs`, `aspire.config.json`, and `apphost.run.json` all exist, and that `apphost.run.json`'s `https` profile carries the dashboard / OTLP / resource-service env vars (full schema is covered by `InitCommand_SingleFileSkeleton_CreatesAppHostRunJsonWithDashboardEnvVars`). 3. `dotnet run apphost.cs` and wait for `Distributed application started.`. 4. Ctrl+C and exit cleanly. Before the fix, `apphost.run.json` was never written, the precondition in step 2 fails fast with an explicit pointer to #15986, and step 3 would crash at startup with the dashboard `OptionsValidationException` about missing `ASPNETCORE_URLS` / `ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL` (verified on this branch — the test-only commit pushed earlier failed in CI with exactly that error before the fix landed in this PR). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Trigger CI revalidation (whitespace only) * Address PR feedback: shared JSON options + ports always agree James review feedback on #16812: 1. Use shared JsonSourceGenerationContext.RelaxedEscaping (which already has WriteIndented = true and UnsafeRelaxedJsonEscaping) instead of inline 'new JsonSerializerOptions { WriteIndented = true }' at both ToJsonString call sites in InitCommand. 2. Make divergence between aspire.config.json and apphost.run.json impossible by construction. DropAspireConfig now returns the effective ports (newly generated, or read back from a pre-existing profiles section), and DropCSharpSingleFileSkeletonAsync threads those into DropAppHostRunJson — so even if aspire.config.json was hand-edited or pre-existed, both files always describe the same dashboard / OTLP / resource service endpoints. Added unit test InitCommand_SingleFileSkeleton_AppHostRunJsonAdoptsPortsFromExistingAspireConfig that pre-seeds aspire.config.json with profiles and asserts apphost.run.json adopts those exact ports. Also dropped the E2E 'Distributed application started.' wait timeout from 7 minutes to 1 minute — even a cold dotnet build of the bare single-file AppHost completes well inside that budget; if it's not started by then, fail fast. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Strengthen inline comments at JSON literals (PR #16812 review nits) James left two nit comments asking for short explanatory comments at the JSON literal blocks. Beef up the comment at the apphost.run.json literal in DropAppHostRunJson explaining what shape it mirrors and why; the existing comment in DropAspireConfig now also calls out that each profile carries the dashboard URL plus the OTLP / resource-service env vars consumed by DashboardOptionsValidator at AppHost startup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR feedback: preserve user profiles + use KnownConfigNames Two more JamesNK review comments on PR #16812: 1. Behavioural regression (line 481): the previous patch overwrote any pre-existing 'profiles' section in aspire.config.json whenever TryReadAppHostProfilePorts couldn't parse all six expected ports (e.g. user-customised config, https-only setup, missing one of the env vars). The original implementation preserved existing profiles unconditionally — this is restoring that safety. New behaviour: - profiles is null -> write fresh, return those ports - profiles parses cleanly -> adopt those ports, return them (existing behaviour) - profiles exists but doesn't match the expected shape -> PRESERVE the user's profiles untouched, generate fresh ports just for apphost.run.json (accepted edge-case divergence — better than silent data loss) Added InitCommand_SingleFileSkeleton_PreservesUnparseableExistingProfiles to lock the preservation behaviour in. 2. Use KnownConfigNames constants (line 546): replaced literal 'ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL', 'ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL', 'ASPIRE_ALLOW_UNSECURED_TRANSPORT' strings throughout InitCommand.cs (in DropAspireConfig, DropAppHostRunJson, and TryReadAppHostProfilePorts) with KnownConfigNames.DashboardOtlpGrpcEndpointUrl / .ResourceServiceEndpointUrl / .AllowUnsecuredTransport. Test fixtures keep literal strings since they document the on-disk JSON shape from the user's perspective. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
9b4bacb to
609774d
Compare
|
Force-pushed a clean rebase of this branch onto current What changed
Verified locally
Note on
|
|
Non-blocking for 13.3, but worth thinking about for main — re: the "unparseable existing profiles" branch in If the user's custom LGTM for 13.3 either way 👍 |
|
Verified manually |
|
No documentation PR is required for this change. This PR is a bug fix that makes
|
Backport of #16812 to release/13.3
/cc @mitchdenny @JamesNK
Customer Impact
After running
aspire initand selecting the C# single-file path,dotnet run apphost.cs(the .NET 10 file-based runner) crashes at startup with:Failed to configure dashboard resource because ASPNETCORE_URLS environment variable was not set.Failed to configure dashboard resource because ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL and ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL environment variables are not set.Customers following the documented
aspire init-> single-file flow can't run their AppHost viadotnet run apphost.csat all.aspire runis unaffected because it injects the env vars fromaspire.config.jsondirectly. Tracked as #15986.Testing
InitCommandTests(3 added covering happy path, port adoption from existingaspire.config.json, and preservation of unparseable user-customized profiles) ΓÇö all 21InitCommandTestspass locally on this backport branch (1m 44s).SingleFileAppHostInitDotnetRunTestsdrives interactiveaspire initand assertsapphost.run.jsonis generated with the expected schema.mainwith all CI green; review feedback from @JamesNK addressed across two rounds (approved).Risk
Low. Scoped to
InitCommand.DropCSharpSingleFileSkeletonAsyncand a new privateDropAppHostRunJsonhelper. New ports parameter onDropAspireConfigis optional and defaults preserve previous behaviour for the (only) other call site. No public API changes, no template changes, no project-locator or config-discovery changes.Regression?
No. The single-file skeleton path was added in 13.3 (new feature for the .NET 10 file-based runner) and never wrote
apphost.run.json; this is the first release that shipsaspire init-> C# single-file, so there's no prior version to regress from. The fix makes the documented flow actually work.Note on conflict resolution: The auto-backport bot couldn't apply this cleanly because PR #16812 was authored on top of #16636 ("Drop nuget.config alongside single-file apphost.cs in
aspire init"), which has not been backported to release/13.3. As of [9b4bacb] this PR also cherry-picks the full #16636 commit (-x 61dac5145) ontorelease/13.3. Without that, the newSingleFileAppHostInitDotnetRunTestsE2E test cannot pass onrelease/13.3— the locally-builtAspire.AppHost.Sdk@13.3.0lives in~/.aspire/hives/run-local/packagesand is invisible to MSBuild without a workspace-localNuGet.config. Stable end-users (after 13.3.0 ships to nuget.org) are unaffected:CreateOrUpdateNuGetConfigWithoutPromptAsyncis a no-op for the defaultstable/implicit channel.The cherry-pick adds the
[QuarantinedTest]markers (#16643) forStopNonInteractiveMultipleAppHostsShowsErrorandStopAllAppHostsFromUnrelatedDirectory, mirroringmain's posture for those tests. The single conflict (insrc/Aspire.Cli/Commands/InitCommand.cs) was resolved by keeping all of #16812's port-generation +DropAspireConfig(effectivePorts)+DropAppHostRunJsonflow intact and inserting the NuGet.config block between"Created apphost.cs"and the port generation.