Skip to content

Polyglot aspire add parity with C# (project-local NuGet.config + auto-select)#17768

Draft
mitchdenny wants to merge 12 commits into
release/13.4from
mitchdenny/ts-template-prerelease-fix
Draft

Polyglot aspire add parity with C# (project-local NuGet.config + auto-select)#17768
mitchdenny wants to merge 12 commits into
release/13.4from
mitchdenny/ts-template-prerelease-fix

Conversation

@mitchdenny
Copy link
Copy Markdown
Member

@mitchdenny mitchdenny commented May 31, 2026

Description

Follow-up to #17743 closing the user-visible gap for polyglot (TS/Python/Go/Java/Rust) apphosts. With a staging-identity CLI:

aspire new        # pick TypeScript Starter
cd <project>
aspire add        # pick foundry

…still offered the older 13.3.5-preview alongside the matching staging version, and when only the staging version was found it still presented a version picker prompt instead of auto-selecting. C# starters did the right thing in both cases.

Root causes

This PR fixes two related-but-distinct bugs that combined to produce the symptom.

1. Polyglot starters didn't drop a project-local NuGet.config

IntegrationPackageSearchService.GetIntegrationPackagesWithChannelsAsync intentionally always includes the Implicit channel so prerelease-only packages (e.g. Foundry) stay reachable when the project is Stable-pinned (#17724 / #17725). The Implicit channel calls dotnet package search with no explicit NuGet config, so it walks the ambient hierarchy.

The C# starter falls back to PromptToCreateOrUpdateNuGetConfigAsync (DotNetTemplateFactory.cs line ~549-552) when no --source was supplied — that writes a project-local NuGet.config pinning Aspire.* to the resolved channel's feed via a package-source mapping. The 4 polyglot template paths only called CreateOrUpdateNuGetConfigForSourceOverrideAsync, which is a no-op without --source. So polyglot projects got no project-local NuGet.config, and the Implicit channel resolved Aspire.Hosting.* from nuget.org → surfaced the stale 13.3.5 alongside the staging match.

2. aspire add showed a version picker on polyglot when C# auto-selected

Two cooperating mistakes in the add flow:

  • IntegrationPackageSearchService included every Explicit channel (stable, daily, staging, ...) whenever the apphost had a pinned channel. That dragged unrelated channels (e.g. daily) and their stale versions into the result set.
  • AddCommand.GetPackageByInteractiveFlow didn't deduplicate by (PackageId, Version) before handing the list to the version prompter. With the project-local NuGet.config from layer 1 in place, the same package+version got surfaced by both Implicit (via the project-local feed map) and the matching pinned Explicit channel, producing two identical rows that prevented the auto-select path in PromptForChannelPackagesAsync from firing.

C# never hit either: IntegrationPackageSearchService.GetConfiguredChannel returns null for C# unconditionally, so the Explicit channel set is never queried for C# add flows.

Fixes

Layer 1 — polyglot starters

Mirror the C# fallback. After the source-override write (which returns false when no --source was supplied), chain CreateOrUpdateNuGetConfigWithoutPromptAsync (the same helper InitCommand uses for polyglot init flows). The helper is a no-op for Implicit / non-Explicit / unregistered channels, so Stable / no-explicit-channel runs are unchanged.

Applied to all 4 affected templates:

  • CliTemplateFactory.TypeScriptStarterTemplate.cs (canonical fix site with full justification comment)
  • CliTemplateFactory.PythonStarterTemplate.cs
  • CliTemplateFactory.GoStarterTemplate.cs
  • CliTemplateFactory.EmptyTemplate.cs (non-csharp branch; the csharp branch was already correct)

Layer 2 — aspire add auto-select parity

Net effect: for a TS apphost pinned to staging, only the Implicit Foundry entry remains after narrowing+dedupe, which trips the explicitGroups.Length == 0 && implicitGroup is not null auto-select branch and skips the version menu — matching the C# flow exactly.

Tests

  • New NewCommandWithTypeScriptStarterAndExplicitChannelWritesChannelDerivedNuGetConfig — drives aspire new aspire-ts-starter --channel staging (no --source) with a registered staging channel containing an Aspire* → darc feed mapping, and asserts a project-local nuget.config lands on disk with the correct package source mapping.
  • New AddCommandWithTypeScriptAppHostPinnedToStagingAutoSelectsImplicitVersionWithoutPromptingForVersion — drives aspire add foundry against a TS apphost pinned to staging, with a fake PackagingService returning Implicit+Stable+Staging+Daily where Implicit and Staging both surface Foundry@13.4.0-preview.1.staging and Daily surfaces the stale Foundry@13.3.5-preview.1.daily. Asserts no IInteractionService selection prompt is raised and that 13.4.0-preview.1.staging is what gets added. Verified fails-without / passes-with both fixes.
  • Updated IntegrationSearchCommandTypeScriptAppHostPersistedChannelExpandsDiscoveryWithoutChangingPreferredResult theory data to reflect the new channel-name-aware narrowing (staging-pin no longer searches an unrelated daily channel).
  • Extended TestTypeScriptStarterProject with an AddPackageAsyncCallback so end-to-end aspire add flows can be exercised in tests.

All 3928 Aspire.Cli.Tests pass.

Validation

Built locally; can be exercised end-to-end with the helper scripts added in #17743:

./eng/scripts/debug-staging.sh \
  --sha e13850995727767ba26088932630342417745f40 \
  --cli ./artifacts/bin/Aspire.Cli/Debug/net10.0/aspire \
  --shell
# inside the subshell:
aspire new                       # TypeScript Starter
cd <generated-project>
ls nuget.config                  # exists now (was missing before)
aspire add foundry               # auto-selects 13.4.0-preview.* (no prompt)

Fixes

Follow-up to #17743 for issue #17744 (polyglot leg).

Targeting release/13.4 since #17743 was merged there; needs forward-port to main.

… resolved

When 'aspire new' resolves an Explicit channel (--channel, or a CLI-identity
channel that PackagingService registered), the C# starter falls back to
PromptToCreateOrUpdateNuGetConfigAsync which writes a project-local
NuGet.config pinning Aspire.* to the channel's feed. The polyglot starters
(TypeScript / Python / Go and the non-csharp branch of the empty template)
only called CreateOrUpdateNuGetConfigForSourceOverrideAsync, which is a
no-op when no --source is supplied.

This left polyglot projects with no project-local NuGet.config, so the
implicit channel that IntegrationPackageSearchService always queries
(intentionally, so prerelease-only packages stay reachable on Stable pins
— see #17724 / #17725) resolved Aspire.Hosting.* from ambient nuget.org
and surfaced a stale version alongside the staging darc-feed match.
PR #17743 routed the staging channel's Aspire.* mapping to the correct
darc feed but did not close this gap because the implicit channel still
went to nuget.org for polyglot apphosts.

Mirror the C# fallback by chaining CreateOrUpdateNuGetConfigWithoutPromptAsync
(the same helper InitCommand uses for polyglot init) when no source override
is supplied. The helper is a no-op for Implicit / unregistered channels so
nothing changes when the resolved channel isn't Explicit.

Adds a regression test exercising the TS starter with a registered staging
channel and no --source, asserting the channel-derived NuGet.config lands
on disk with the Aspire* → channel-feed mapping.

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

github-actions Bot commented May 31, 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 -- 17768

Or

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

@mitchdenny mitchdenny changed the base branch from main to release/13.4 May 31, 2026 22:23
Polyglot apphosts (TypeScript / Python / Go) that pin a channel via
`aspire.config.json` were still presenting a multi-row version picker on
`aspire add` (e.g. Implicit 13.4.0-preview / staging 13.4.0-preview /
daily 13.3.5-preview) while the equivalent C# flow auto-selected the
pinned channel's version. Two root causes working together:

1. `IntegrationPackageSearchService.GetIntegrationPackagesWithChannelsAsync`
   included EVERY explicit channel (stable, daily, staging, ...) whenever
   the apphost had a pinned channel. That dragged unrelated channels
   (e.g. daily) and their stale versions into the result set.

2. `AddCommand.GetPackageByInteractiveFlow` did not deduplicate by
   (PackageId, Version) before handing the list to the version prompter.
   With layer 1 in place, polyglot starters that drop a project-local
   NuGet.config (PR #17768) get the same package+version from both
   Implicit (via the project-local feed map) and the pinned Explicit
   channel, producing two identical rows that prevented the auto-select
   path in `PromptForChannelPackagesAsync` from firing.

Fixes:
- IPSS narrows the Explicit channel set to the apphost-pinned channel
  only (Implicit always stays so prerelease-only packages remain
  reachable when the pin is Stable-quality — preserving the
  #17724/#17725 guarantee).
- AddCommand dedupes `orderedPackageVersions` by (Id, Version) keeping
  the first occurrence (Implicit-channel entry wins due to existing
  ordering).

Net effect: for a TS apphost pinned to staging, only the Implicit
Foundry entry remains, which trips the
`explicitGroups.Length == 0 && implicitGroup is not null` auto-select
branch and skips the version menu — exactly matching the C# flow.

Tests:
- New regression test
  `AddCommandWithTypeScriptAppHostPinnedToStagingAutoSelectsImplicitVersionWithoutPromptingForVersion`
  asserts no IInteractionService selection prompt is raised and that
  the correct staging version is added. Verified fails-without /
  passes-with.
- Updated existing
  `IntegrationSearchCommandTypeScriptAppHostPersistedChannelExpandsDiscoveryWithoutChangingPreferredResult`
  theory data to reflect the new channel-name-aware narrowing
  (staging-pin no longer searches an unrelated daily channel).
- Extended TestTypeScriptStarterProject with an
  `AddPackageAsyncCallback` so end-to-end `aspire add` flows can be
  exercised in tests.

All 3928 Aspire.Cli.Tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny changed the title Drop project-local NuGet.config for polyglot starters when channel is resolved Polyglot aspire add parity with C# (project-local NuGet.config + auto-select) May 31, 2026
mitchdenny and others added 5 commits June 1, 2026 08:53
…r rc files

The --shell subshell sets PATH="$cli_dir:$PATH" before exec'ing the user's
interactive shell. But `zsh -i` (and `bash -i`) source the user's startup
files AFTER our environment is in place, and a very common line in those
files is:

  export PATH="$HOME/.aspire/bin:$PATH"

That re-prepend pushes the installed `~/.aspire/bin/aspire` ahead of our
locally built CLI in the subshell, so `which aspire` (and every `aspire`
invocation) silently picks up the wrong binary -- breaking validation.

Fix: drop a tiny shim (`<tempdir>/bin/aspire` exec'ing the absolute
--cli path) and start the subshell with rc files that source the user's
real rc and THEN prepend the shim dir. zsh uses ZDOTDIR; bash uses
--rcfile; other shells fall back to the previous env-only PATH export
with a warning pointing at the shim path so the developer can invoke
it directly.

The shim dir is cleaned up when the subshell exits.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The overrideCliIdentityChannel and overrideCliInformationalVersion diagnostic
env vars (set by eng/scripts/debug-staging.sh to validate staging feed routing
from a locally built CLI) were previously honored only inside PackagingService:
the synthesized staging channel was correctly routed at the SHA-specific darc
feed, but the rest of the CLI still saw the baked 'local' identity and the
local assembly version. The two half-emulations combined to produce two visible
bugs in 'aspire new':

  1. NewCommand auto-selected the Implicit (nuget.org) channel instead of the
     synthesized staging channel because ExecutionContext.IdentityChannel
     returned 'local' (no registered channel). The Implicit channel only sees
     the previous shipped stable Aspire.ProjectTemplates (e.g. 13.3.5) so a
     local CLI emulating staging stamped 13.3.5 into the new project's
     aspire.config.json.
  2. The 'CLI version differs from configured SDK version' warning fired even
     when the user had aligned both via the override, because
     VersionHelper.GetDefaultTemplateVersion read the raw assembly attribute.

Fixes:
  - IPackagingService.GetEffectiveIdentityChannel exposes the existing
    override-aware identity lookup. NewCommand consults it (with a fallback to
    ExecutionContext.IdentityChannel) when picking the default channel.
  - VersionHelper.GetDefaultTemplateVersion honors overrideCliInformationalVersion
    so template selection, SDK pinning, and the CLI banner all report the
    emulated version.
  - debug-aspire-channel.sh auto-detects the actual Aspire.ProjectTemplates
    version published to the SHA's darc feed and uses that as the default
    when --version is not supplied. Without this the user has to hand-pick a
    version that matches what darc published for that SHA (which depends on
    whether the build was stabilized), and a mismatch silently degrades
    template selection to the highest nuget.org version (13.3.5).

Tests:
  - Added GetEffectiveIdentityChannelCallback to TestPackagingService; the
    default returns null/empty so existing channel-resolution tests fall
    through to ExecutionContext.IdentityChannel unchanged.
  - Verified end-to-end via 'debug-staging.sh --shell' that 'aspire new
    aspire-ts-starter' now writes 13.4.0 and 'aspire add foundry' auto-selects
    the staging-feed prerelease without prompting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address review feedback: instead of calling Environment.GetEnvironmentVariable
directly inside VersionHelper, plumb CliExecutionContext through so the
overrideCliInformationalVersion diagnostic override is read via
context.GetEnvironmentVariable, matching how other staging-validation env
vars flow through the CLI.

- VersionHelper.GetDefaultTemplateVersion / GetDefaultSdkVersion /
  TryGetCurrentCliVersionMatch now take an optional CliExecutionContext?
  parameter. When supplied, env reads route through context; otherwise
  fall back to process env so the ~30 unrelated callers don't churn.
- PackageChannel constructor and Create*Channel factories accept an
  optional CliExecutionContext? and pass it to VersionHelper.
- PackagingService passes _executionContext to all PackageChannel
  factory calls and to its own VersionHelper.GetDefault* calls.
- NewCommand passes ExecutionContext into TryGetCurrentCliTemplateVersionPackage
  so VersionHelper sees the staging override.
- GuestAppHostProject CLI/SDK skew check passes _executionContext.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On a stabilizing release branch (e.g. release/13.4) the same SHA's darc feed
can publish core packages stable-shaped ("13.4.0") and integration packages
prerelease-shaped ("13.4.0-preview.1.X.Y"). The script stamps the CLI's
informational version from the templates package, so the resulting stable
shape makes PackagingService's _isStableShapedCliVersion() return true, which
auto-selects staging-channel quality=Stable. That quality filter then hides
the prerelease integration packages, so `aspire add foundry` only sees
nuget.org's previous shipped version and the staging channel silently
disappears from the version-selection menu.

Exporting overrideStagingQuality=Both bypasses the auto-derived quality and
keeps all matching versions on the darc feed eligible -- which is what
end-to-end validation actually wants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Polyglot apphosts (TS/Python/Go) deliberately don't carry a persistent
NuGet.config — that file is a .NET-ism. Restore is done via on-the-fly
`TemporaryNuGetConfig`s built from `aspire.config.json#channel` (see
`PrebuiltAppHostServer.CreatePackageSourceOverrideNuGetConfigAsync`).

The IntegrationPackageSearchService was unconditionally including the
Implicit channel whenever the apphost had a pin, even for polyglot. Because
polyglot has no project-local NuGet.config, Implicit's `dotnet package
search` ran against the user's ambient (typically nuget.org) and surfaced
stale/wrong-feed versions alongside the pinned channel's versions. This
produced the spurious 'select version' prompt the user reported on TS
apphosts pinned to staging (#17743), and also added 'staging' / 'daily'
rows to stable packages like azure-appcontainers.

Fix: when a polyglot apphost has a pin, search ONLY the pinned channel.
The pinned channel materializes its own TemporaryNuGetConfig from its PSM
mappings, so it's correctly scoped. This matches C# end-to-end: C#'s
`GetConfiguredChannel` returns null, so IPSS runs Implicit against the C#
template's project-local NuGet.config (which IS correctly scoped because
the C# template writes one).

Trade-off knowingly accepted: a stable-pinned polyglot apphost cannot
reach prerelease-only packages (e.g. Foundry). This matches C# behavior —
a stable-pinned C# project also wouldn't see them.

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

github-actions Bot commented Jun 1, 2026

Re-running the failed jobs in the CI workflow for this pull request because 3 jobs were identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

Matched test failure patterns (26 tests)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.TracingEnablesTheRightActivitySource — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.TracingEnablesTheRightActivitySource_Keyed — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.OptionsTypeIsSealed — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.HealthChecksRegistersHealthCheckService(enabled: True) — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.HealthChecksRegistersHealthCheckService(enabled: False) — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.EachKeyedComponentRegistersItsOwnHealthCheck — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.TracingRegistersTraceProvider(enabled: True) — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.TracingRegistersTraceProvider(enabled: False) — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.MetricsRegistersMeterProvider(enabled: True) — MCR container pull denied (rate limiting)
  • Aspire.Microsoft.Data.SqlClient.Tests.ConformanceTests.MetricsRegistersMeterProvider(enabled: False) — MCR container pull denied (rate limiting)
  • ...and 16 more

mitchdenny and others added 2 commits June 1, 2026 11:09
A shipped (identity=stable) CLI running 'aspire new' was writing
aspire.config.json#channel = "stable", which routed subsequent
'aspire add' calls through the Stable PackageChannel
(PackageChannelQuality.Stable, * -> nuget.org). That filter hides
prerelease packages on nuget.org, so a community 13.4.0-preview
integration the team hadn't yet shipped as stable became invisible
after scaffolding — for both the TypeScript polyglot starter (cell 7)
and the C# aspire-starter family (cell 8).

'aspire init' already avoided the trap via
PackageChannel.ShouldPersistChannelName (Type == Explicit AND name !=
"stable"). 'aspire new' had two persistence sites that only checked
Type == Explicit:
  * ResolveCliTemplateVersionAsync (CLI-runtime polyglot path)
  * ResolveIdentityChannelNameAsync (DotNet-runtime path consumed by
    DotNetTemplateFactory)

Both now use ShouldPersistChannelName, matching aspire init. Explicit
'--channel stable' still wins via the channel-arg leg of the resolution
coalesce — only the identity-fallback case is changed.

Two pre-existing InlineData rows in NewCommandChannelResolutionTests
codified the buggy behavior (one in #17573 alongside daily stabilization
work, one in #17637 alongside the DotNet-runtime forwarding fix); both
were rolled in defensively rather than as a deliberate stable-pin
invariant. Removed and replaced with two dedicated negative tests
(_DoesNotPersistStablePin_Cli / _DoesNotPersistStablePin_DotNet) that
were red before the production fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`get-aspire-cli-pr.{sh,ps1}` installs PR builds to
`$INSTALL_PREFIX/dogfood/pr-<N>/bin/aspire` (see compute_cli_install_dir /
Get-CliInstallDir), not `$INSTALL_PREFIX/bin/aspire`. The PATH
auto-discovery fallback in debug-aspire-channel.{sh,ps1} only knew about
`~/.aspire/bin/aspire`, so when `--pr` was supplied it would install the
PR build into the dogfood path, then pick whatever existing `aspire` was
already first on PATH (typically the user's stable install at
`~/.aspire/bin/aspire`) and shim that one instead. Net result: `--pr`
appeared to do nothing.

When `--pr` is supplied, pin cli_path / $Cli to the dogfood install
location and only fall back to PATH discovery if that file isn't found
(in which case warn loudly — the install script's layout has likely
shifted). An explicit `--cli` / `-Cli` still overrides everything.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny marked this pull request as ready for review June 1, 2026 02:55
Copilot AI review requested due to automatic review settings June 1, 2026 02:55
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

Follow-up to #17743 that closes the polyglot (TS/Python/Go/Java/Rust) gap so aspire new + aspire add matches the C# experience on staging-identity CLIs. Without the fix, polyglot starters got no project-local NuGet.config, and aspire add could surface stale stable versions alongside the staging match (or render a spurious version picker that C# never showed).

Changes:

  • Polyglot starter templates (TS, Python, Go, Empty's non-csharp branch) now fall back to a channel-derived NuGet.config (CreateOrUpdateNuGetConfigWithoutPromptAsync) when no --source is supplied, mirroring DotNetTemplateFactory.
  • IntegrationPackageSearchService narrows the channel set to only the apphost-pinned channel when one is configured (Implicit and other explicit channels are excluded), restoring C# end-to-end parity for aspire add.
  • Adds IPackagingService.GetEffectiveIdentityChannel() and threads it through NewCommand so the overrideCliIdentityChannel diagnostic env var (used by eng/scripts/debug-staging.sh) is honored end-to-end; VersionHelper.GetDefault{Template,Sdk}Version and PackageChannel are correspondingly plumbed with CliExecutionContext so the parallel overrideCliInformationalVersion override flows through; persist-channel decisions switch from Type is Explicit to the existing ShouldPersistChannelName() (which also excludes "stable").
Show a summary per file
File Description
src/Aspire.Cli/Templating/CliTemplateFactory.TypeScriptStarterTemplate.cs Fallback to channel-derived NuGet.config when no --source.
src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs Same fallback for Python starter.
src/Aspire.Cli/Templating/CliTemplateFactory.GoStarterTemplate.cs Same fallback for Go starter.
src/Aspire.Cli/Templating/CliTemplateFactory.EmptyTemplate.cs Same fallback in the non-csharp branch.
src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs Narrow channel set to pinned channel when configured.
src/Aspire.Cli/Commands/AddCommand.cs Materialize orderedPackageVersions to array for multiple enumeration.
src/Aspire.Cli/Commands/NewCommand.cs Use GetEffectiveIdentityChannel + ShouldPersistChannelName() gating.
src/Aspire.Cli/Packaging/PackagingService.cs Expose GetEffectiveIdentityChannel on the interface; thread CliExecutionContext into channels.
src/Aspire.Cli/Packaging/PackageChannel.cs Accept optional CliExecutionContext for version-override plumbing.
src/Aspire.Cli/Utils/VersionHelper.cs Honor overrideCliInformationalVersion env var via execution context.
src/Aspire.Cli/Projects/GuestAppHostProject.cs Pass execution context to GetDefaultSdkVersion.
eng/scripts/debug-aspire-channel.{sh,ps1} Auto-detect feed version, install-path pinning for --pr, shim PATH for interactive shells, set overrideStagingQuality.
tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs New staging auto-select test; rewrite existing tests to assert the narrowed-channel contract.
tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs New regression test asserting channel-derived NuGet.config is written for TS starter.
tests/Aspire.Cli.Tests/Commands/NewCommandChannelResolutionTests.cs Add stable-identity-does-not-pin tests; drop stable cases from existing theory data.
tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs Implement new GetEffectiveIdentityChannel member.
tests/Aspire.Cli.Tests/TestServices/TestTypeScriptStarterProjectFactory.cs Add AddPackageAsyncCallback hook for aspire add tests.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 0

aspire init was reading _executionContext.IdentityChannel directly for
three feed-routing decisions:

  1. C# single-file: NuGet.config drop next to apphost.cs
  2. C# project: NuGet.config drop in the solution directory
  3. Polyglot (and C# single-file): channel persisted into
     aspire.config.json and forwarded to ScaffoldContext.Channel, which
     the prebuilt apphost server uses to scope the dynamically generated
     NuGet.config sources

That bypassed PackagingService.GetEffectiveIdentityChannel(), which is
the only place that consults the 'overrideCliIdentityChannel' diagnostic
env var used by eng/scripts/debug-staging.sh / debug-stable.sh. As a
result, a CLI binary baked as 'stable' (or 'local') being emulated as
'staging' would drop a NuGet.config pinned to the wrong channel, and the
follow-up 'aspire add foundry' (which DOES go through PackagingService)
would resolve against feeds that init had never written, so it found
only 13.3.5 instead of the 13.4.0 staging prerelease from the DARC feed.

Also route the template-package query's RequestedChannel through the
same helper so the C# project flow asks dotnet new install against the
effective channel's source.

Hive-lookup / ChannelNotFoundException paths still read the baked
identity directly: those are physical-binary properties, not feed
routing.

Adds three regression tests that bake identity=local, register both
'local' and 'staging' channels, set the effective-channel override to
'staging', and assert init now drops a NuGet.config scoped to the
staging feed (with packageSourceMapping for Aspire*) and hands
'staging' to the polyglot scaffolder.

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

I don't think we should be dropping a nuget.config file in the TypeScript apphost. I think a reasonable alternative would be to add a new section to aspire.config.json with this information.

Counterpart fix to the InitCommand change in e836a5b: NewCommand's
ResolveIdentityChannelNameAsync was still reading
ExecutionContext.IdentityChannel directly, so on the DotNet-runtime
template path (C# aspire-starter, including the Blazor starter) under
the eng/scripts/debug-staging.sh emulator the chain ran:

  raw identity = 'local'
    -> no Explicit channel named 'local' registered
    -> ResolveIdentityChannelNameAsync returns null
    -> TemplateInputs.Channel = null
    -> DotNetTemplateFactory's TemplatePackageQuery.RequestedChannel = null
    -> TemplateNuGetConfigService walks Implicit (nuget.org)
    -> Aspire.ProjectTemplates resolves to last-shipped stable (13.3.5)
       instead of the staging build's prerelease on the darc feed.

PackagingService.GetEffectiveIdentityChannel already accounts for the
overrideCliIdentityChannel diagnostic env var; route through it (with
the same baked-identity fallback used by ResolveCliTemplateVersionAsync)
so the DotNet path matches the CLI-runtime path's behavior the staging
fix originally established. Mirrors the parallel
InitCommand.GetEffectiveFeedRoutingChannel helper.

Adds two regression tests in NewCommandChannelResolutionTests:
  * NewCommand_LocalIdentityWithStagingOverride_DotNet_PropagatesEffectiveChannel
    is the bug-asserting test (red before this commit).
  * NewCommand_LocalIdentityWithStagingOverride_Cli_PropagatesEffectiveChannel
    is a no-regression test for the CLI-runtime path that was already
    override-aware.

Both go through an extended BuildPackagingService that wires
TestPackagingService.GetEffectiveIdentityChannelCallback and registers
the override channel rather than the baked identity, matching the
PackagingService shape under debug-staging.sh.

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

joperezr commented Jun 1, 2026

13.4.0 is closed now, let's look at this one for 13.4.1 especially since it seems to be staging specific.

The shell-mode emulator redirects NUGET_PACKAGES to a per-sha isolated
directory so a simulated staging build's Aspire.* restore can't
contaminate the developer's real global cache. But NuGet only reads
from NUGET_PACKAGES on lookup, so transitive non-Aspire dependencies
that are version-stable across feeds — most notably
Microsoft.DeveloperControlPlane.<rid> (DCP) and .NET runtime packs —
have nowhere to come from. The polyglot apphost server then fails to
launch with:

  Could not invoke 'run': The Aspire orchestration component is not
  installed at "$NUGET_PACKAGES/microsoft.developercontrolplane.<rid>/<ver>/tools/dcp".

Wire NUGET_FALLBACK_PACKAGES at the developer's real cache
(~/.nuget/packages by default, overridable via the same env var
pre-set). NuGet treats fallback folders as read-only on lookup after
NUGET_PACKAGES, so DCP and runtime packs resolve from the existing
cache while any staging-shaped Aspire.* package still lands ONLY in
the isolated dir — keeping the contamination guarantee intact.

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

Are we confident that stable is going to work? If we don't need this then I don't want to merge it - in 13.5 I want to rip most of the guts out of this stuff becuase its completely untestable right now.

@mitchdenny mitchdenny marked this pull request as draft June 1, 2026 03:53
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

CLI E2E Tests unknown — 110 passed, 0 failed, 2 unknown (commit 00a067f)

View all recordings
Status Test Recording Job Artifacts
AddPackageInteractiveWhileAppHostRunningDetached Recording #78784730474 Logs
AddPackageWhileAppHostRunningDetached Recording #78784730474 Logs
AgentCommands_AllHelpOutputs_AreCorrect Recording #78784730351 Logs
AgentInitCommand_DefaultSelection_InstallsDefaultSkills Recording #78784730351 Logs
AgentInitCommand_MigratesDeprecatedConfig Recording #78784730351 Logs
AgentInitCommand_NonInteractive_BundleOnlySkillsBeyondCliCatalog_AreInstallable Recording #78784730351 Logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp Recording #78784730873 Logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost Recording #78784730873 Logs
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated Recording #78784730873 Logs
AllPublishMethodsBuildDockerImages Recording #78784730782 Logs
AspireAddAndStartWorkAgainstLegacyAppHostTs Recording #78784730480 Logs
AspireAddPackageVersionToDirectoryPackagesProps Recording #78784730891 Logs
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost Recording #78784730273 Logs
AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAndPreservesFiles Recording #78784730345 Logs
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive Recording #78784730345 Logs
AspireStartUpdatesStaleTypeScriptAppHostPath Recording #78784730645 Logs
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps Recording #78784730891 Logs
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent Recording #78784730891 Logs
Banner_DisplayedOnFirstRun Recording #78784730700 Logs
Banner_DisplayedWithExplicitFlag Recording #78784730700 Logs
Banner_NotDisplayedWithNoLogoFlag Recording #78784730700 Logs
CertificatesClean_RemovesCertificates Recording #78784730298 Logs
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate Recording #78784730298 Logs
CertificatesTrust_WithUntrustedCert_TrustsCertificate Recording #78784730298 Logs
ConfigSetGet_CreatesNestedJsonFormat Recording #78784730832 Logs
CreateAndRunAspireStarterProject Recording #78784730479 Logs
CreateAndRunAspireStarterProjectWithBundle Recording #78784730847 Logs
CreateAndRunEmptyAppHostProject Recording #78784730339 Logs
CreateAndRunJavaEmptyAppHostProject Recording #78784730448 Logs
CreateAndRunJsReactProject Recording #78784730863 Logs
CreateAndRunPolyglotAppHostWithDevLocalhostUrls Recording #78784730479 Logs
CreateAndRunPythonReactProject Recording #78784730478 Logs
CreateAndRunTypeScriptEmptyAppHostProject Recording #78784730433 Logs
CreateAndRunTypeScriptStarterProject Recording #78784730240 Logs
CreateJavaAppHostWithViteApp Recording #78784730585 Logs
CreateTypeScriptAppHostWithViteApp_AllowsGuestAppPackageManagerToDiffer Recording #78784730442 Logs
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain Recording #78784730442 Logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces Recording #78784730791 Logs
DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost Recording #78784730791 Logs
DashboardRunWithOtelTracesReturnsNoTraces Recording #78784730791 Logs
DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost Recording #78784730791 Logs
DeployK8sBasicApiService Recording #78784730554 Logs
DeployK8sWithExternalHelmChart Recording #78784730416 Logs
DeployK8sWithGarnet Recording #78784730732 Logs
DeployK8sWithMongoDB Recording #78784730366 Logs
DeployK8sWithMySql Recording #78784730506 Logs
DeployK8sWithPostgres Recording #78784730341 Logs
DeployK8sWithRabbitMQ Recording #78784730921 Logs
DeployK8sWithRedis Recording #78784730660 Logs
DeployK8sWithSqlServer Recording #78784730335 Logs
DeployK8sWithValkey Recording #78784730446 Logs
DeployTypeScriptAppToKubernetes Recording #78784730277 Logs
DescribeCommandResolvesReplicaNames Recording #78784730736 Logs
DescribeCommandShowsRunningResources Recording #78784730736 Logs
DetachFormatJsonProducesValidJson Recording #78784730836 Logs
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance Recording #78784730836 Logs
DoPublishAndDeployListStepsWork Recording #78784730521 Logs
DocsCommand_RendersInteractiveMarkdownFromLocalSource Recording #78784730805 Logs
DoctorCommand_DetectsDeprecatedAgentConfig Recording #78784730351 Logs
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain Recording #78784730311 Logs
DoctorCommand_WithSslCertDir_ShowsTrusted Recording #78784730311 Logs
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted Recording #78784730311 Logs
GatewayWithoutExternalEndpoint_FailsPublishWithGuidance Recording #78784730898 Logs
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain Recording #78784730442 Logs
GlobalMigration_HandlesCommentsAndTrailingCommas Recording #78784730832 Logs
GlobalMigration_HandlesMalformedLegacyJson Recording #78784730832 Logs
GlobalMigration_PreservesAllValueTypes Recording #78784730832 Logs
GlobalMigration_SkipsWhenNewConfigExists Recording #78784730832 Logs
GlobalSettings_MigratedFromLegacyFormat Recording #78784730832 Logs
IngressWithoutExternalEndpoint_FailsPublishWithGuidance Recording #78784730898 Logs
InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdirectory Recording #78784730442 Logs
InteractiveCSharpInitCreatesExpectedFiles Recording #78784730741 Logs
InvalidAppHostPathWithComments_IsHealedOnRun Recording #78784730890 Logs
JavaScriptHostingApisRunFromTypeScriptAppHost Recording #78784730782 Logs
LatestCliCanStartStableChannelAppHost Recording #78784730479 Logs
LatestCliCanStartStableChannelTypeScriptAppHost Recording #78784730479 Logs
LegacySettingsMigration_AdjustsRelativeAppHostPath Recording #78784730645 Logs
LogsCommandShowsResourceLogs Recording #78784730520 Logs
OtelLogsReturnsStructuredLogsFromStarterApp Recording #78784730834 Logs
OtelLogsReturnsStructuredLogsFromStarterAppIsolated Recording #78784730834 Logs
PsCommandListsRunningAppHost Recording #78784730488 Logs
PsFormatJsonOutputsOnlyJsonToStdout Recording #78784730488 Logs
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts Recording #78784730453 Logs
PublishWithConfigureEnvFileUpdatesEnvOutput Recording #78784730453 Logs
PublishWithDockerComposeServiceCallbackSucceeds Recording #78784730453 Logs
PublishWithoutOutputPathUsesAppHostDirectoryDefault Recording #78784730453 Logs
ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogContainsEntries Recording #78784730841 Logs
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput Recording #78784730841 Logs
RestoreGeneratesSdkFiles Recording #78784730422 Logs
RestoreGeneratesSdkFiles_WithConfiguredToolchain Recording #78784730376 Logs
RestoreRefreshesGeneratedSdkAfterAddingIntegration Recording #78784730376 Logs
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes Recording #78784730384 Logs
RunFromParentDirectory_UsesExistingConfigNearAppHost Recording #78784730869 Logs
RunReportsSyntaxErrorsForDotNetAppHost Recording #78784730708 Logs
RunReportsSyntaxErrorsForTypeScriptAppHost Recording #78784730708 Logs
SecretCrudOnDotNetAppHost Recording #78784730477 Logs
SecretCrudOnTypeScriptAppHost Recording #78784730872 Logs
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels Recording #78784730406 Logs
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets Recording #78784730721 Logs
StartReportsSyntaxErrorsForDotNetAppHost Recording #78784730708 Logs
StartReportsSyntaxErrorsForTypeScriptAppHost Recording #78784730708 Logs
StopAllAppHostsFromAppHostDirectory Recording #78784730499 Logs
StopJavaPolyglotAppHostUsingApphostDirectory Recording #78784730438 Logs
StopNonInteractiveSingleAppHost Recording #78784730499 Logs
StopTypeScriptPolyglotAppHostUsingApphostDirectory Recording #78784730316 Logs
StopWithNoRunningAppHostExitsSuccessfully Recording #78784730474 Logs
TypeScriptAppHostRunDoesNotDeadlockWhenLazyOptionsInvokeAsyncCallback Recording #78784730433 Logs
UnAwaitedChainsCompileWithAutoResolvePromises Recording #78784730376 Logs
UpdateProjectChannelToStable_CSharpEmptyAppHost_PreservesAspireConfigChannel Recording #78784730377 Logs
UpdateProjectChannelToStable_CSharpSingleFileInit_PreservesAspireConfigChannel Recording #78784730377 Logs
UpdateProjectChannelToStable_TypeScriptSingleFileInit_PreservesAspireConfigChannel Recording #78784730377 Logs
UpdateProjectChannelToStable_TypeScript_PreviewsStablePackagesAndPreservesChannel Recording #78784730377 Logs

📹 Recordings uploaded automatically from CI run #26734047521

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.

4 participants