Skip to content

fix(cli): keep guest AppHost restore on the selected Aspire package source#17166

Merged
radical merged 26 commits into
microsoft:mainfrom
radical:radical/issue-17159-aspire-empty-typescript-hive
May 19, 2026
Merged

fix(cli): keep guest AppHost restore on the selected Aspire package source#17166
radical merged 26 commits into
microsoft:mainfrom
radical:radical/issue-17159-aspire-empty-typescript-hive

Conversation

@radical
Copy link
Copy Markdown
Member

@radical radical commented May 16, 2026

Description

What broke

Guest-language aspire new scaffolding could restore AppHost/codegen packages from a different Aspire feed than the one selected by the CLI invocation.

The explicit failure was:

aspire new aspire-empty --language typescript --source <pr-hive>/packages --version <pr-version>

Even though the user supplied the PR hive, restore could float to a different preview package set and TypeScript code generation failed with:

RPC server exception:
System.TypeLoadException: Could not load type 'Aspire.TypeSystem.AtsJsonCodeWriter' from assembly 'Aspire.TypeSystem, Version=42.42.42.42, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'.

The same mismatch could happen in the normal PR dogfood flow from #17225:

aspire new aspire-empty --language typescript

when the running CLI was acquired from a PR hive but the prebuilt guest AppHost restore did not use the matching local Aspire package source.

Root cause

aspire new resolved the template version/channel, and it parsed --source, but those source/channel decisions were not consistently carried into the prebuilt guest AppHost restore.

That left NuGet free to consider normal channel feeds for Aspire* packages. Because prerelease versions are minimum ranges unless exact-pinned, restore could pick a different Aspire package set than the CLI assemblies doing codegen.

Fix

The guest AppHost restore path now keeps Aspire* packages on the selected Aspire source:

  • explicit --source is threaded through empty and starter guest-language scaffolding into prebuilt AppHost restore
  • PR/local hive channels with local Aspire* mappings are auto-discovered as an effective source override when no --source was supplied
  • Aspire* packages are exact-pinned when using an explicit or auto-discovered local source, while non-Aspire dependencies keep normal restore semantics
  • package source mapping is carried through both package-only restore and project-reference closure restore
  • non-Aspire fallback sources, requested-channel mappings, and staging requested-channel mappings are preserved
  • --source URLs that carry credentials (userinfo, query string, or fragment) are rejected up front with a dedicated error rather than being silently written into the project's NuGet.config
  • restore failure output includes redacted source/channel/package context to make Aspire*-source mismatches diagnosable without leaking secrets

Call-outs

--source is still a one-shot scaffold restore input. It is not written into the generated project, so later aspire restore / aspire add use the project's channel configuration.

The #17225 fix depends on the PR/local acquisition flow registering a matching channel with an existing local Aspire* package source. If that hive/channel is absent, the CLI still falls back to the existing channel/ambient restore behavior.

Fixes #17159
Refs #17225

Validation

  • dotnet test --project tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj --no-launch-profile -- --filter-class "*.Projects.PrebuiltAppHostServerTests" --filter-class "*.Commands.NewCommandTests" --filter-class "*.Commands.AddCommandTests" --filter-class "*.Projects.AppHostServerProjectTests" --filter-class "*.Scaffolding.ChannelReseedTests" --filter-class "*.Packaging.PackagingServiceTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"
  • dotnet test --project tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj --no-launch-profile -- --filter-method "*.NewCommand_NoChannelArg_PrChannelIdentity_ResolvesTemplateFromPrChannel" --filter-method "*.ChannelPinningTemplate_IdentityMatchesRegisteredChannel_PinsThatChannel" --filter-method "*.PrepareAsync_WithHiveBackedChannel_UsesLocalAspireSourceAsOverride" --filter-method "*.PrepareAsync_RestoreFailure_WithAutoDiscoveredLocalSource_FooterShowsEffectiveSource" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"

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

aspire new aspire-empty --language typescript --source <pr-hive>
--version <pr-version> parsed --source, but the empty AppHost
TypeScript scaffolding path dropped it before the prebuilt AppHost
restored Aspire.Hosting and TypeScript code-generation packages. The
bundled restore then searched channel sources, missed the requested PR
hive packages, and NuGet floated to a stale preview package set, which
later failed with TypeLoadException when the generator loaded against
the PR CLI's Aspire.TypeSystem.

Flow TemplateInputs.Source into ScaffoldContext, pass it to
IAppHostServerProject.PrepareAsync, and have PrebuiltAppHostServer add
that source to package and closure restores. When a source override is
present, use exact version ranges so restore fails rather than silently
resolving a different Aspire prerelease.

Fixes microsoft#17159

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

github-actions Bot commented May 16, 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 -- 17166

Or

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

@radical
Copy link
Copy Markdown
Member Author

radical commented May 16, 2026

There is a second part of the fix that improves this when --source is not passed. And it will be included in #17105 .

Remove whitespace-only changes that are unrelated to the source restore fix, keeping the PR focused on the explicit package source propagation.

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

This comment was marked as outdated.

@radical radical marked this pull request as ready for review May 16, 2026 06:02
Copilot AI review requested due to automatic review settings May 16, 2026 06:02
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 aspire new aspire-empty --language typescript failures when callers supply --source (e.g., a PR hive) by threading the source override through the empty-template scaffolding path into AppHost server preparation, and ensuring the prebuilt AppHost restore uses that override (including exact-version restores when an explicit source is provided) to avoid mixed-binary package sets.

Changes:

  • Thread TemplateInputs.Source through ScaffoldContext into IAppHostServerProject.PrepareAsync(...) as packageSourceOverride.
  • Update PrebuiltAppHostServer restore logic to include the explicit source for both package-only restore and project-closure restore, and restore exact versions when an explicit source is provided.
  • Add/adjust tests to validate the override is passed through and that restore is invoked with the expected sources/version constraints.

Reviewed changes

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

Show a summary per file
File Description
tests/Aspire.Cli.Tests/TestServices/FakeFailingAppHostServerProject.cs Updates fake to match new PrepareAsync signature with source override.
tests/Aspire.Cli.Tests/Scaffolding/ChannelReseedTests.cs Adds coverage that scaffolding passes PackageSourceOverride into PrepareAsync.
tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs Adds coverage that bundled restore uses --source override and exact-version package args.
tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs Updates test fake to match the updated PrepareAsync signature.
src/Aspire.Cli/Templating/CliTemplateFactory.EmptyTemplate.cs Passes inputs.Source into ScaffoldContext.PackageSourceOverride for empty templates.
src/Aspire.Cli/Scaffolding/ScaffoldingService.cs Forwards PackageSourceOverride into IAppHostServerProject.PrepareAsync.
src/Aspire.Cli/Scaffolding/IScaffoldingService.cs Extends ScaffoldContext with PackageSourceOverride.
src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs Applies source override to restore paths; enforces exact version restore when override is set.
src/Aspire.Cli/Projects/IAppHostServerProject.cs Extends PrepareAsync contract to accept an optional packageSourceOverride.
src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs Updates signature to satisfy the extended IAppHostServerProject contract.

When an explicit package source is passed to guest AppHost restore, keep the exact-version pinning scoped to Aspire packages because that is the source mapping being overridden. Non-Aspire integration packages should retain normal NuGet minimum-version restore semantics.

Also apply the temporary NuGet.config to the project-reference closure restore path instead of only adding sources to the synthetic project. That keeps package source mapping and channel-specific restore settings active when package and project integrations are restored together.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@radical radical added this to the 13.4 milestone May 16, 2026
/// </summary>
internal sealed class PrebuiltAppHostServer : IAppHostServerProject
{
private const string NuGetOrgSource = "https://api.nuget.org/v3/index.json";
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.

why?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why the constant? It is being used in a few places.

radical and others added 4 commits May 18, 2026 13:10
Resolve the PrebuiltAppHostServerTests conflict by keeping both the new main coverage for CLI log path propagation and the PR helper used by source override tests.

Also address PR feedback on the NuGet.org fallback by documenting why the explicit source override keeps NuGet.org available for non-Aspire packages and transitive dependencies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…spire-empty-typescript-hive

Two conflicts resolved manually:

- src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs: adopted
  origin/main's new IPackagingService.GetChannelsAsync signature
  (the extra requestedChannel filter argument) inside the PR's
  extracted GetExplicitRestoreChannelsAsync helper.
- tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs:
  kept both branches' new tests
  (PrepareAsync_WithPackageReferences_UsesPackageSourceOverride and
  PrepareAsync_WithProjectReferencesAndPackageSourceOverride_UsesNuGetConfig
  from this branch, plus
  PrepareAsync_WithStagingPinnedProjectOutsideLaunchDirectory_UsesStagingSourcesAndNuGetConfig
  from origin/main).

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

@danegsta danegsta left a comment

Choose a reason for hiding this comment

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

Found 3 issues around source/channel/feed resolution.

var packages = packageRefs
.Select(r => (r.Name, Version: GetRestoreVersion(r.Name, r.Version!, useExactPackageVersions)))
.ToList();
using var temporaryNuGetConfig = await TryCreateTemporaryNuGetConfigAsync(requestedChannel, packageSourceOverride, cancellationToken);
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.

When --source is provided this temp config is generated with <clear />, and then passed to restore, so we stop honoring any nuget.config in the app directory. That breaks cases where the app config contains credentials, private feeds, or package source mappings for non-Aspire integration packages. Can we merge the source override into the effective config (or use an additive source/mapping approach) so the user's config remains in play, and include the requested source/channel/config in restore failures?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks — looked into the blast radius before changing this.

Today packageSourceOverride only reaches PrebuiltAppHostServer.PrepareAsync from one call site: ScaffoldingService.PrepareAsync (the aspire new aspire-empty --language <non-csharp> path). The other four callers — SdkGenerateCommand, SdkDumpCommand, GuestAppHostProject (used by aspire run/aspire restore/aspire add), and AppHostServerSession — all pass the default null. Plus, of the two ScaffoldContext construction sites, only CliTemplateFactory.EmptyTemplate forwards inputs.Source; InitCommand does not.

So the <clear />-emitting temp config kicks in at exactly one moment in the user's flow: the initial scaffold restore for aspire new --source. At that point the project directory is being freshly created — there is no project-local nuget.config to clobber. The pre-existing channel-driven <clear /> behavior on later commands is unchanged by this PR.

That makes the credentials / private-feeds / non-Aspire-PSM concern theoretical for the surface this PR introduces. Restore-failure context is a separate ask — that part is fair, and I've taken it on (see below).

If I've missed a path that flows --source into a context where a user-owned nuget.config is present, happy to fix it — please point me at it.

For the restore-failure-context piece of your comment, the next push enriches the PrepareAsync failure output with the --source, channel, and package context, so a failed aspire new --source <X> no longer requires a verbose re-run to see which inputs were in play. New test: PrepareAsync_RestoreFailure_OutputIncludesSourceAndChannelContext.

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.

Do we have any scenarios where our restore would interact with global nuget.config (or a nuget.config in a parent folder) after new? I'm mainly wondering if there's the potential for surprise if aspire new behaves differently from something like aspire add due to new using a specific config?

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.

new is supposed to write a nuget.config file based on the default or specified channel then add should use that nuget.config or channel.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks, this is the behavior the branch now moves toward: aspire new --source writes a project nuget.config so later aspire add/aspire restore can consume the same source state instead of treating --source as one-shot. It deliberately does not import parent/user/global config into the generated project.

One auth detail remains worth calling out: we reject embedded credentials, but sanitized URLs may not bind to user-level packageSourceCredentials if those credentials are keyed by a named source. We should decide whether to adopt an existing ambient source key for the same URL, rely on credential providers, or use another NuGet-native pattern.

Comment thread src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs Outdated
Comment thread src/Aspire.Cli/Templating/CliTemplateFactory.EmptyTemplate.cs
Copy link
Copy Markdown
Member

@danegsta danegsta left a comment

Choose a reason for hiding this comment

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

Additional test coverage request.

Comment thread tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs
radical and others added 3 commits May 18, 2026 23:06
When `aspire new aspire-empty --source <pr-hive>` ran without an
explicit `--channel`, the temp NuGet.config built for restore folded in
every explicit channel's `Aspire* -> channelSource` mapping alongside
the override's `Aspire* -> packageSourceOverride`. NuGet treats
same-pattern mappings on multiple sources as co-eligible, so Aspire
packages could still resolve from a channel feed and silently defeat
the override's fail-fast intent. Exact-version pinning masked this in
practice because the requested PR-hive version was usually unique, but
the package source mapping itself was no longer exclusive to the
override.

In the override branch of `TryCreateTemporaryNuGetConfigAsync`, only
fold in mappings from an explicitly-requested, matched channel (skip
the catch-all "all explicit channels" fallback baked into
`GetExplicitRestoreChannelsAsync`), and drop any `Aspire*`-prefixed
mappings from that matched channel before merging. Non-Aspire patterns
(`CommunityToolkit*`, catch-all `*`) are preserved so non-Aspire
transitives keep their channel feeds. Mirror the same gating in
`GetNuGetSourcesAsync` so the bundled NuGet service's `sources` list
doesn't broadcast every channel feed when `--source` is the override
mechanism.

Add seven `TryCreateTemporaryNuGetConfig_*` test cases covering the
override-with-channels matrix (no channel, matched channel, channel
with `Aspire*` mapping, channel with all-packages mapping, lookup
failure, requested-channel threading) plus a `PrepareAsync_*`
integration check for the NuGet.org fallback. `TestPackagingService`
gains a `LastRequestedChannelName` observable so the new
`PassesRequestedChannelToPackagingService` test can assert the
override branch threads `requestedChannel` into the packaging service.

Touch the comments near `NuGetOrgSource` and the
`RestoreConfigFile`/`RestoreAdditionalProjectSources` split so they
describe the actual constraint ("cannot float to NuGet.org or any
other co-eligible feed").

Refs microsoft#17159

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Running `aspire new aspire-empty --language <non-csharp> --source <X>`
succeeds at scaffold time but the override is consumed only for the
initial restore inside `PrebuiltAppHostServer`. The scaffolded project
persists only the channel and SDK version, so a follow-up `aspire add`
or `aspire restore` in the same project resolves Aspire packages from
the channel feeds in `aspire.config.json` rather than `<X>` — and
silently produces a different package set, or fails when the channel
does not carry the requested version.

Emit a yellow warning immediately after the scaffold succeeds (when
`inputs.Source` is non-empty on the non-C# branch) so users supplying
`--source <pr-hive>/packages` are not surprised when subsequent
commands miss the override. Persisting the feed into a generated
`nuget.config` (and also honoring `--source` on the C# empty path,
which silently drops it today) is left as a follow-up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the prebuilt AppHost scaffold restore fails, the displayed output
is the only debugging surface most users see. Previously it carried
only the raw NuGet stderr ("Failed to prepare: Package restore failed:
..."), with no record of which `--source`, channel, or package
versions had been in play. Reproducing the failure required a verbose
re-run with diagnostic logging just to recover the inputs.

Append the override source, the requested channel, and a short
preview of the package list to the `OutputCollector` from both of
`PrepareAsync`'s catch blocks (`AppHostServerPrepareFailedException`
and the catch-all wrapper around `RestoreNuGetPackagesAsync`). When
neither `--source` nor a channel was specified the helper is a no-op,
so existing failure messages without these inputs are unchanged.

Add an end-to-end `PrepareAsync` test that wires `--source` together
with a channel whose `Aspire*` mapping conflicts with the override
and asserts the temp `nuget.config` actually passed to the restore
invocation drops the channel's `Aspire*` mapping, pinning that the
override is authoritative for `Aspire*` packages end-to-end (and not
only at the temp-config generator unit boundary).

Add a `PrepareAsync_RestoreFailure_OutputIncludesSourceAndChannelContext`
test that fails the restore via a non-zero exit and asserts the
override path, channel name, and package id are present in the
returned output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
radical and others added 4 commits May 19, 2026 00:07
`aspire new aspire-{ts,py,go}-starter --source <pr-hive> --version <pr>` hit
the same TypeLoadException class of failure as `aspire-empty` did before the
fix landed in this branch: the override was plumbed into
PrebuiltAppHostServer.PrepareAsync for the empty-template path only, while
starter templates went through GuestAppHostProject.BuildAndGenerateSdkAsync
→ PrepareAppHostServerAsync without forwarding the override, so Aspire
packages restored from channel feeds rather than the requested source.

Thread `packageSourceOverride` through IGuestAppHostSdkGenerator.BuildAndGenerateSdkAsync
and the GuestAppHostProject prepare helper, then pass `inputs.Source` from
all three guest starter templates. Hoist the "override is not persisted"
warning into a shared helper on CliTemplateFactory so the empty and starter
paths emit the same message; the warning fires only after a successful
scaffold so it doesn't add noise behind a more prominent restore failure.

Tests:
- Expand the empty-template warning test to a [Theory] covering TypeScript
  and Java (the latter behind the experimental polyglot flag).
- Add starter-template coverage for both the warning+plumb-through happy
  path and the failed-restore-suppresses-warning path.
- Pin the restore-failure context footer shape (`--source:`, `channel:`,
  `packages:` labels) and the >5-package truncation behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The shared `DisplaySourceOverrideNotPersistedWarningIfNeeded` helper on
`CliTemplateFactory` is invoked by both `aspire-empty` and the three
guest-language starter templates (TypeScript, Python, Go), so the
`Empty*` prefix on the resource key is stale. Drop the prefix while
the string is still pre-release and re-translation has not yet been
triggered for translators.

Renames the resource in `.resx`, `.Designer.cs`, the single production
call site in `CliTemplateFactory.cs`, and four references in
`NewCommandTests.cs` (empty and starter happy-path + suppression
cases). `dotnet build /t:UpdateXlf src/Aspire.Cli/Aspire.Cli.csproj`
regenerates the 13 `*.xlf` files to pick up the new `trans-unit id`.

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

`TryCreateTemporaryNuGetConfigAsync` already drops the matched
channel's `Aspire*` mapping in the override branch, pinning Aspire
package restoration to `--source` exclusively. But `GetNuGetSourcesAsync`
— which builds the `--source` CLI argument list passed alongside the
temp config — was still iterating every mapping in the matched
channel and adding each mapping URL, including the channel's Aspire
feed. The bundled NuGet tool treats `--source` CLI args as co-eligible
with config mappings (which is why the original "don't fold in every
explicit channel" comment exists in this method), so re-adding the
channel's Aspire feed silently undoes the temp config's PSM drop and
lets Aspire packages still resolve from the channel feed.

A second, smaller divergence: when the matched channel had no
`*` (AllPackages) mapping, the temp config added `* -> NuGet.org` as
a catch-all but the sources list's `sources.Count == 1` heuristic
only added NuGet.org in the no-channel case, leaving a mismatched
catch-all whenever a matched channel contributed any non-Aspire
mapping (e.g. `CommunityToolkit*`, `Microsoft.*`).

In the matched-channel loop, skip mappings whose `PackageFilter`
starts with "Aspire" when an override is set, and observe whether
the matched channel supplied its own AllPackages mapping. After the
loop, fall back to NuGet.org only when no AllPackages mapping was
seen — the same rule the temp config uses for its catch-all.

Tests:

- `GetNuGetSources_WithPackageSourceOverrideAndMatchedChannel_OmitsChannelAspireFeedFromSources`
  pins that the channel's Aspire feed URL does NOT appear in the
  `--source` argument list, even though the channel maps `Aspire*`
  to it. This is the inverse assertion of the existing
  `TryCreateTemporaryNuGetConfig_WithPackageSourceOverride_DropsRequestedChannelAspireMappings`
  test on the config side.
- `..._KeepsChannelSourceAndAddsNuGetOrgFallback` covers the
  `CommunityToolkit*` case: non-Aspire channel mapping stays, and
  NuGet.org is added because the matched channel has no AllPackages
  mapping.
- `..._OmitsNuGetOrgFallback` covers a channel that already supplies
  `* -> channelSource`: NuGet.org should NOT be added, because the
  channel's own AllPackages mapping is the catch-all in both the
  temp config and the sources list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
radical and others added 4 commits May 19, 2026 01:49
When --source isn't supplied, PrebuiltAppHostServer now resolves the
requested channel and, if it has a hive-backed Aspire* mapping pointing
at an existing local directory, uses that as the package source
override for both package-only and project-reference restore.

That closes the dogfood gap where `aspire new aspire-empty --language
typescript` from a PR/local CLI would resolve Aspire packages through
the ambient channel feed instead of the CLI's own hive, surfacing as
TypeLoadException during code generation.

Channel-lookup failures are swallowed-and-logged (mirroring the
existing defensive catches in TryCreateTemporaryNuGetConfigAsync and
GetNuGetSourcesAsync); OperationCanceledException is re-thrown.

Tests cover the explicit-channel-only path, the explicit-source-wins
path, and that http-backed channels keep their existing non-exact
restore behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…spire-empty-typescript-hive

Resolved conflicts in PrebuiltAppHostServer.cs and its tests:

- `GetNuGetSourcesAsync` and `TryCreateTemporaryNuGetConfigAsync`
  visibility widened to `internal` (from microsoft#17235) while keeping this
  branch's `packageSourceOverride` parameter.
- Channel-name comparisons updated to `StringComparisons.ChannelName`
  to match the shared comparer introduced on main.
- Kept both sets of tests in the overlap region: this branch's
  `--source` override tests plus microsoft#17235's staging-unavailable refusal
  tests. Adjusted the two new staging tests that call
  `GetNuGetSourcesAsync` directly to pass `packageSourceOverride: null`
  for the widened signature.

Verified: `dotnet build tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj` succeeds with 0 errors / 0 warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A post-merge review of microsoft#17166 (saved under .squad/log) flagged five issues
in the prebuilt + DotNet-based AppHost restore paths that survived the
`origin/main` merge. This addresses them.

**#1 (HIGH) — Project-ref restore replaced ambient nuget.config for any
non-Local explicit channel.** `BuildIntegrationClosureManifestAsync`
called `TryCreateTemporaryNuGetConfigAsync` for every explicit channel,
which emitted `<RestoreConfigFile>` and replaced nuget.config discovery
wholesale. A user with a private/internal feed in their ambient
nuget.config and a `daily` or `pr-*` channel pin would silently lose
that feed during project-ref restore. Now only synthesize a temp
nuget.config when `--source` is set; otherwise add channel sources via
`<RestoreAdditionalProjectSources>` so the ambient nuget.config is
preserved.

**#2 (HIGH) — DotNetBasedAppHostServerProject accepted
`packageSourceOverride` but ignored it.** The in-repo / dogfood path
(selected whenever `AspireRepositoryDetector.DetectRepositoryRoot`
returns non-null) declared the parameter to satisfy
`IAppHostServerProject` but never threaded it into restore. The
template factory was unconditionally telling users
`--source was used for the initial scaffold restore only…` even when
the override had been silently dropped. Thread the override through
`CreateProjectFilesAsync` and prepend it to the
`<RestoreAdditionalProjectSources>` list so the hive is the first
source NuGet evaluates. This path does not use Package Source Mappings
(PSM) like `PrebuiltAppHostServer` does — in dev mode most Aspire.*
dependencies come from `ProjectReference` and the override is best-
effort for the rare `PackageReference` fallback. Documented inline.

**#3 (MED) — Restore-failure footer showed the original `--source`,
not the auto-discovered effective one.** When `--source` was not
passed but `ResolveLocalPackageSourceOverrideAsync` auto-discovered a
local hive, the catches in `PrepareAsync` passed the original
(unset) `packageSourceOverride` argument to
`AppendRestoreContextOnFailure`. The user saw only the channel name
and had no signal that a local hive participated in the failed
restore. Lift `effectivePackageSourceOverride` to outer scope and
pass it to the catches.

**#4 (MED) — `BundleNuGetService` logged raw `--source` to the debug
log.** The full restore args (including credentialed feed URLs) were
emitted as a single debug line that downstream `RedactSourceForDisplay`
never touched. Now build a redacted copy of the args specifically for
the log line — the verbatim args still go to the process. Handles
repeated `--source` flags and a missing trailing value defensively.

**#5 (MED) — `RedactSourceForDisplay` failed open on malformed
credentialed URLs.** `Uri.TryCreate` returns false for
`https://user:p@ss@host/path` and `https://user:p#word@host/`
(confirmed empirically), and the redactor's parse-failure branch
returned the raw input. Such inputs were guaranteed to leak
credentials into the failure footer that ships in bug reports. Fail
closed for HTTP-shaped inputs by detecting `http://` / `https://`
prefix before parsing and returning `<unparseable http source>` when
the parse fails. Plain non-HTTP inputs (local paths, file://, etc.)
still pass through unchanged.

Refactor: extract `RedactSourceForDisplay` into a shared
`PackageSourceRedactor` utility so the same redaction is applied
wherever sources appear in user-visible output. `PrebuiltAppHostServer`
keeps the internal static alias for back-compat with existing tests.

Tests added:
- `PrepareAsync_WithProjectReferencesAndExplicitChannelButNoOverride_UsesAdditionalSourcesNotRestoreConfigFile`
- `PrepareAsync_RestoreFailure_WithAutoDiscoveredLocalSource_FooterShowsEffectiveSource`
- `RedactSourceForDisplay_FailsClosedForMalformedHttpButPassesThroughLocalPaths` (5 inline cases)
- `CreateProjectFiles_WithPackageSourceOverride_PrependsOverrideToRestoreAdditionalProjectSources`
- `CreateProjectFiles_WithoutPackageSourceOverride_DoesNotInjectExtraSource`

All 3297 tests in Aspire.Cli.Tests pass (0 failures, 20 platform skips).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…icrosoft#17227 merge

PR microsoft#17227's defensive catch around the channel-lookup helper had two
call sites; only the auto-discovery one survived the merge into this
branch. The PSM-temp-config no-override path still propagates a
transient `IPackagingService.GetChannelsAsync` failure out to
`PrepareAsync`'s outer catch, turning a transient packaging-service
hiccup (malformed `aspire.config.json`, unexpected feed probe error)
into a hard `aspire new` scaffold failure.

Mirror the existing defensive catch into the no-override branch of
`TryCreateTemporaryNuGetConfigAsync`: cancellation rethrows, anything
else logs and returns null so restore falls through to the ambient
nuget.config + caller-resolved channel sources path, matching the
catch in `ResolveLocalPackageSourceOverrideAsync` and the long-standing
catch in `GetNuGetSourcesAsync`.

Restore the dropped degrade test (`PrepareAsync_WhenPackagingService-
ThrowsDuringAutoDiscovery_DegradesGracefully`) so a future refactor
can't silently regress this back. Also add two negative-path tests
for `aspire-empty --language <guest>` source-coherence:

- `PrepareAsync_WithHiveBackedChannelPointingAtMissingLocalDirectory_-
  DoesNotApplyOverride` pins that a stale `aspire.config.json` (user
  deleted the local hive but the channel pin remains) does not pin
  Aspire packages to a non-existent directory or emit exact-pin /
  NuGet.org fallback.

- Extend `NewCommandWithEmptyTemplateAndSourceOverrideWarnsThatOverride-
  IsNotPersisted` to also cover python, go, and rust, matching the
  five guest languages registered in `DefaultLanguageDiscovery`.

Refs microsoft#17159

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@radical radical changed the title fix(cli): honor --source for TypeScript empty AppHost restore fix(cli): keep guest AppHost restore on the selected Aspire package source May 19, 2026
@radical
Copy link
Copy Markdown
Member Author

radical commented May 19, 2026

Since the last pushed revision, this branch now covers the broader guest AppHost source-coherence scope:

  • Expanded the fix from explicit --source <pr/local hive> to the no---source PR/local dogfood path tracked by PR-acquired Aspire CLI should use matching PR package source for empty AppHost templates #17225. When a registered PR/local channel has an existing local Aspire* source, prebuilt guest AppHost restore now auto-discovers it and treats it as the effective Aspire source.
  • Extended --source plumbing beyond aspire-empty into guest-language starter templates, and added a warning that --source is only used for the initial scaffold restore and is not persisted for later aspire restore / aspire add.
  • Tightened restore source behavior so the explicit or auto-discovered source owns Aspire* packages exclusively, while non-Aspire fallbacks, requested-channel mappings, global packages folder settings, and staging requested-channel behavior from origin/main are preserved.
  • Added restore-failure context showing the effective source/channel/package versions, with HTTP source credentials/query strings redacted before display.
  • Added regression coverage for the explicit --source path, PR/local hive auto-discovery path, project-reference restore path, source redaction, one-shot warning, requested-channel/staging behavior, and channel lookup fallback behavior.

Validation rerun after the latest branch shape:

dotnet test --project tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj --no-launch-profile -- --filter-class "*.Projects.PrebuiltAppHostServerTests" --filter-class "*.Commands.NewCommandTests" --filter-class "*.Commands.AddCommandTests" --filter-class "*.Projects.AppHostServerProjectTests" --filter-class "*.Scaffolding.ChannelReseedTests" --filter-class "*.Packaging.PackagingServiceTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"

Result: 250 passed.

I also validated the #17225 claim with the focused path tests:

dotnet test --project tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj --no-launch-profile -- --filter-method "*.NewCommand_NoChannelArg_PrChannelIdentity_ResolvesTemplateFromPrChannel" --filter-method "*.ChannelPinningTemplate_IdentityMatchesRegisteredChannel_PinsThatChannel" --filter-method "*.PrepareAsync_WithHiveBackedChannel_UsesLocalAspireSourceAsOverride" --filter-method "*.PrepareAsync_RestoreFailure_WithAutoDiscoveredLocalSource_FooterShowsEffectiveSource" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"

Result: 12 passed.

@radical radical marked this pull request as ready for review May 19, 2026 07:29
@radical radical requested a review from eerhardt May 19, 2026 07:29
@radical
Copy link
Copy Markdown
Member Author

radical commented May 19, 2026

Open for re-review.

@radical
Copy link
Copy Markdown
Member Author

radical commented May 19, 2026

PR Testing Report

PR Information

CLI Version Verification

  • Expected (PR head): 057abfaa
  • Installed reports: 13.4.0-pr.17166.g057abfaa on both targets ✅
  • Hive: Aspire.*.13.4.0-pr.17166.g057abfaa.nupkg present (all expected packages incl. Aspire.AppHost.Sdk, Aspire.Cli, Aspire.Hosting, plus a wide spread of integrations)

Changes Analyzed

Pure CLI change. All edits are in src/Aspire.Cli/ — notably Projects/PrebuiltAppHostServer.cs (+321/-32) and a new Utils/PackageSourceRedactor.cs, plus --source threading through CliTemplateFactory.{Empty,GoStarter,PythonStarter,TypeScriptStarter}Template.cs + base factory, a new TemplatingStrings resource entry (with xlf translations), and substantial test additions in tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs and Commands/NewCommandTests.cs. Note: the PR diff includes CliTemplateFactory.GoStarterTemplate.cs, but this hive does not register an aspire-go-starter template (confirmed on both linux-arm64 and osx-arm64 RIDs), so Go scenarios were not exercisable here.

Test Scenarios Executed

Each scaffold scenario ran with the PR CLI binary and confirmed no Aspire.TypeSystem.AtsJsonCodeWriter / TypeLoadException in the install or scaffold output. End-to-end scenarios additionally exercised aspire run and the deep ~/.aspire/logs/cli_*.log for the runtime path.

Scaffold (container, linux-arm64)

# Scenario Template / Language --source Result
1 TS empty, explicit hive (PR body repro) aspire-empty --language typescript yes ✅ PASS
2 TS empty, auto-discovered local source aspire-empty --language typescript no ✅ PASS
3 TS starter, explicit hive aspire-ts-starter yes ✅ PASS
4 TS starter, auto-discovered local source aspire-ts-starter no ✅ PASS
5 Py starter, explicit hive aspire-py-starter yes ✅ PASS
6 Py starter, auto-discovered local source aspire-py-starter no ✅ PASS

Each scaffold produced the expected template artifacts (e.g. for TS empty: apphost.ts, tsconfig.apphost.json, aspire.config.json, .modules/{base,aspire,transport}.ts; for Py starter: app/{main.py,pyproject.toml,telemetry.py} + frontend/{App.tsx,vite.config.ts,…} + the TS guest apphost.ts at the root).

End-to-end aspire run (host, osx-arm64)

# Scenario What was exercised Result
7 TS empty repro + aspire run Scaffold + auto npm installConnecting to AppHost… → dashboard up at https://localhost:17005/ (HTTP 302 login redirect). Deep log: no failure signature, no error/exception/fatal lines anywhere. ✅ PASS
8 Py starter + aspire run Scaffold + auto npm install for frontend + uv venv for backend. Deep log: GuestAppHostProject Running TypeScript (Node.js) AppHost: …/py-starter-source/apphost.tsDistributed application started. Full stack live and visible in ps: node tsx apphost.ts (TS guest AppHost) + python uvicorn main:app --ssl-keyfile … --ssl-certfile … (FastAPI over HTTPS) + vite --port 63475 (React frontend). Dashboard at https://localhost:17000/ (HTTP 302). ✅ PASS

Observations

  • aspire new aspire-empty --language typescript fails with TypeLoadException when using PR hive packages #17159 explicit repro is fixed. Scenarios 1 and 7 are the exact failing command from the PR body. They now succeed with no TypeLoadException, and the new warning ⚠️ --source was used for the initial scaffold restore only and is not persisted… (new TemplatingStrings entry) is emitted.
  • PR-acquired Aspire CLI should use matching PR package source for empty AppHost templates #17225 auto-discovery path works. Scenarios 2, 4, 6 use only aspire new <template> (no --source), relying on the PR hive's channel registration carrying a local Aspire* package source through restore. All succeed.
  • Cross-template coverage at scaffold time. Both TS empty and TS starter, plus the Python starter (which uses a TypeScript guest AppHost internally — the exact code path being fixed), all succeed in the container against this hive.
  • Cross-language coverage at runtime. Scenario 8 is the strongest signal for "languages other than TypeScript": with no manual setup beyond aspire new + aspire run, the Python starter brought up a real multi-process distributed app on osx-arm64 — Python FastAPI bound to its HTTPS port and the React vite dev server bound to its port — orchestrated by the TS guest AppHost that the PR's restore changes feed. The deep log's Distributed application started. line is the canonical "AppHost RPC bootstrapped and codegen succeeded" signal that the original AtsJsonCodeWriter failure would have blocked.
  • Failure signature absent everywhere. Aspire.TypeSystem.AtsJsonCodeWriter, TypeLoadException, and Could not load type do not appear in any scaffold log (6 container scenarios), any aspire run console output (2 host scenarios), or any deep ~/.aspire/logs/cli_*.log for those runs.
  • Benign warnings / log noise (not regressions):
    • Container scaffolds: ❌ npm is not installed or not found in PATH — environmental (no Node.js in the runner image); the Aspire scaffold and Aspire-package restore itself completed before this point in every case.
    • Host aspire run: one Kestrel SSL exception (System.IO.IOException: The encryption operation failed, inner Bad address) appeared in the deep log after curl --max-time 5 https://localhost:17000/ aborted a TLS handshake mid-flight. Pure client-disconnect artifact in the dashboard's Kestrel pipeline, unrelated to the fix.

Limitations

  • Go starter not exposed in this hive. aspire new --help lists only aspire-starter, aspire-ts-cs-starter, aspire-ts-starter, aspire-empty, aspire-ts-empty, aspire-py-starter on both linux-arm64 and osx-arm64 RIDs. The Go-side changes in CliTemplateFactory.GoStarterTemplate.cs therefore could not be exercised end-to-end against PR fix(cli): keep guest AppHost restore on the selected Aspire package source #17166's published artifacts — worth flagging whether the registration is gated, deliberately deferred, or just not yet shipped.

Overall Result

✅ PR VERIFIED on both container (linux-arm64) and host (osx-arm64), at both scaffold and aspire run levels. The originally failing TypeScript scaffolding path now succeeds with both explicit --source and auto-discovered local Aspire source, and a non-trivial multi-process distributed app (Python FastAPI + React/vite + TypeScript guest AppHost) starts end-to-end against the PR build.

Copy link
Copy Markdown
Member

@IEvangelist IEvangelist left a comment

Choose a reason for hiding this comment

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

Two findings on the new --source / auto-discovery plumbing in PrebuiltAppHostServer.cs. Both are inline. The first is a behavioral inconsistency I think is worth fixing in this PR; the second is an accuracy nit on a comment that became stale after the auto-discovery commit landed.

Comment thread src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
Comment thread src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs Outdated
Comment thread src/Aspire.Cli/Projects/IAppHostServerProject.cs Outdated
Comment thread src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs Outdated
Comment thread src/Aspire.Cli/Utils/PackageSourceRedactor.cs
Comment thread src/Aspire.Cli/Utils/PackageSourceRedactor.cs Outdated
radical and others added 6 commits May 19, 2026 13:37
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep cancellation tokens last on the guest AppHost prepare/source APIs now
that both requested channel and source override are threaded through the
same calls.

Move the staging-unavailable guard before temporary NuGet.config creation
so the source-override project-reference restore path cannot silently
fall back to NuGet.org when staging cannot be synthesized. Also update
the project-reference restore comment to describe both explicit --source
and auto-discovered local channel sources.

Add direct PackageSourceRedactor coverage for happy paths, malformed
HTTP inputs, whitespace-prefixed HTTP sources, and non-HTTP source forms.
Trim HTTP inputs before detection/parsing so indented feed URLs are still
redacted or fail closed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
An explicit `aspire new --source <source>` previously only affected the
initial scaffold restore. The generated project did not record that source,
so later `aspire add` or `aspire restore` could fall back to channel or
ambient NuGet configuration and lose the Aspire package source selected at
creation time.

Persist source overrides into the generated project's NuGet.config by
mapping `Aspire*` to the explicit source and keeping non-Aspire fallback
sources from the resolved channel, or NuGet.org when no channel fallback is
available. The persisted config remains self-contained: it does not import
parent, user, or global NuGet sources, mappings, disabled sources, or
credentials; only an existing project-local NuGet.config is merged.

Remove the stale warning that source overrides are not persisted, share the
source-override mapping logic with the prebuilt restore path, and add tests
covering empty templates, starter templates, .NET templates, existing config
merge behavior, and ambient-config non-absorption.

Refs microsoft#17159
Refs microsoft#17225

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Persisting `aspire new --source` into a project NuGet.config makes the
source durable project state. Credential-bearing HTTP URLs should not be
written there because the generated file can be committed accidentally.

Reject HTTP(S) sources that contain user info, query strings, or fragments
before project creation starts, and keep the lower-level mapping helper from
persisting those sources if it is called directly. The error points users at
NuGet credential providers or user-level NuGet configuration instead of
embedding secrets in the feed URL.

Refs microsoft#17159
Refs microsoft#17225

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The NuGet config merger no longer maps wildcard package resolution to the
PR hive when a separate fallback source already owns `*`. Update the PR-hive
snapshots so CI expects Aspire packages only from the hive and keeps the
fallback mapping on the appropriate source.

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

aspire new aspire-empty --language typescript fails with TypeLoadException when using PR hive packages

5 participants