Fix aspire init template install for non-stable CLI builds (#16654) - #16690
Conversation
…ilds (#16654) �spire init ran `dotnet new install Aspire.ProjectTemplates@<cliVersion+sha>` with `nugetConfigFile: null` and `nugetSource: null`, bypassing the channel feed wiring used by `aspire new`. For non-stable CLI builds (staging/daily/PR), `Aspire.ProjectTemplates@<cliVersion+sha>` is only available on a per-commit darc feed (e.g. `darc-pub-microsoft-aspire-<sha8>`), so install failed with exit code 103 in any C# repo containing a `.sln`. Extract the channel-aware template package resolution and install logic out of `DotNetTemplateFactory.ApplyTemplateAsync` and into `TemplateNuGetConfigService` as `ResolveTemplatePackageAsync` and `InstallTemplatePackageAsync`. Both `DotNetTemplateFactory` and `InitCommand` now consume the helper. The existing `aspire new` install path is preserved bit-for-bit (extraction is mechanical; `IncludePrHives: true` keeps PR-hive widening behavior). For `aspire init` this means: - The version sent to `dotnet new install` is now the channel-resolved one (e.g. `13.3.0`), not the raw `+sha` build metadata. - Init now honors the global `channel` configuration, matching `aspire new`. - On install failure, captured stdout/stderr is displayed before the error. - `ChannelNotFoundException` and `EmptyChoicesException` produce friendly errors instead of bubbling to the top-level "unexpected error" handler. - PR hives are intentionally NOT included in init's channel discovery so a developer with stale `~/.aspire/hives/*` doesn't get a different template than they'd get on a clean machine. Notes: - `TemplateNuGetConfigService` is a singleton; `IDotNetCliRunner` is transient and is therefore passed as a method parameter to `InstallTemplatePackageAsync` instead of being injected. - New regression tests cover: explicit channel passes the temp NuGet config, implicit channel leaves it null, PR hives don't widen init, and channel resolution failures produce friendly errors. Fixes #16654 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Multi-model code review caught the following issues: 1. Restore original order of operations in DotNetTemplateFactory.ApplyTemplateAsync. The first refactor moved extraArgsCallback ahead of template package resolution, which changed prompt/error precedence for `aspire new` (extra-args prompts like Redis-cache, test-framework, xUnit-version would now run before channel lookup, and answers would be discarded if resolution failed afterward). Restored the BEFORE order from release/13.3: ResolveTemplatePackageAsync first, then extraArgsCallback, then InstallTemplatePackageAsync. Updated the in-source comment to be accurate. 2. Catch NuGetPackageCacheException in InitCommand.DropCSharpProjectSkeletonAsync. The pre-extraction init code went straight to `dotnet new install` and never invoked a NuGet search, so feed search failures (offline, inaccessible feed, etc.) couldn't bubble up. After the extraction init now performs the search and was missing the catch, surfacing the failure as an unhandled "unexpected error". Added the catch with the same friendly-error treatment as ChannelNotFoundException / EmptyChoicesException. 3. Use TemplatingStrings.TemplateInstallationFailed in InitCommand for parity with `aspire new`. The previous ad-hoc string omitted the log file path, making post-mortem diagnosis harder. 4. Added a comment in InstallTemplatePackageAsync clarifying that the temporary NuGet config is intentionally disposed at the end of the install (only `dotnet new install` consumes it; the subsequent `dotnet new <template>` call uses the already-installed template hive and ambient NuGet config). 5. Added regression test InitCommand_WhenChannelTemplateSearchFails_DisplaysFriendlyError covering the new NuGetPackageCacheException catch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16690Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16690" |
There was a problem hiding this comment.
Pull request overview
This PR forward-ports the release/13.3 fix for #16654 to main, ensuring aspire init can install Aspire.ProjectTemplates for non-stable CLI builds by reusing the same channel-aware NuGet feed resolution logic as aspire new.
Changes:
- Extracts template package channel/version resolution +
dotnet new installinvocation intoTemplateNuGetConfigService, and updates bothDotNetTemplateFactoryandInitCommandto use it. - Improves
aspire initerror handling for channel resolution / package search failures and surfaces install output on failure. - Updates CLI test infrastructure and adds init-focused unit tests covering explicit/implicit channels, PR-hive suppression, and friendly errors.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | Registers TemplateNuGetConfigService for tests and updates DotNetTemplateFactory wiring to use DI-resolved instance. |
| tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs | Extends install-template test callback signature to include nugetConfigFile. |
| tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs | Updates factory construction to match new TemplateNuGetConfigService constructor. |
| tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs | Updates template install callbacks for the new runner signature. |
| tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs | Adds coverage for init’s channel-aware template install behavior + friendly error paths. |
| src/Aspire.Cli/Templating/TemplateNuGetConfigService.cs | Adds ResolveTemplatePackageAsync + InstallTemplatePackageAsync and supporting records for channel-aware template installation. |
| src/Aspire.Cli/Templating/DotNetTemplateFactory.cs | Removes inlined channel/template install logic and delegates to TemplateNuGetConfigService. |
| src/Aspire.Cli/Commands/InitCommand.cs | Switches init’s project-mode template install to channel-aware resolution + improved error reporting. |
| services.AddTransient(options.DotNetCliExecutionFactoryFactory); | ||
| services.AddTransient(options.DotNetCliRunnerFactory); | ||
| services.AddTransient(options.NuGetPackageCacheFactory); | ||
| services.AddSingleton<TemplateNuGetConfigService>(); |
There was a problem hiding this comment.
Good catch — this is a forward-port artifact. The cherry-pick from #16672 added the registration on a new line at the bottom of the block, but main already had the same registration from #16636 a few lines up. Removed the duplicate in 6d08933 (kept the original main registration so the diff in this PR is purely additive elsewhere).
| using Aspire.Cli.Scaffolding; | ||
| using Aspire.Cli.Tests.TestServices; | ||
| using Aspire.Cli.Tests.Utils; | ||
| using Aspire.Shared; |
There was a problem hiding this comment.
False positive — Aspire.Shared is needed here. NuGetPackageCli (used at lines 34, 35, 435, 436, 554, 555, 560, 561, 627, 628 of this file) is declared in the Aspire.Shared namespace. The local build of this PR with TreatWarningsAsErrors=true succeeds with 0 warnings, which confirms it.
Cherry-pick from #16672 added services.AddSingleton<TemplateNuGetConfigService>() to CliTestHelper.cs, but main already had the same registration from #16636. Drop the duplicate (the cherry-picked one) so the service is only registered once. Caught by Copilot PR review on #16690. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
|
🎬 CLI E2E Test Recordings — 76 recordings uploaded (commit View all recordings
📹 Recordings uploaded automatically from CI run #25239849198 |
Jose Perez Rodriguez (joperezr)
left a comment
There was a problem hiding this comment.
Thanks Mitch!
|
/deployment-test |
|
❌ Deployment E2E Tests failed — 27 passed, 6 failed, 0 cancelled View test results and recordings
|
|
No documentation PR is required for this change. This is an internal bug fix that corrects
|
…#16654) (microsoft#16690) * [release/13.3] Fix aspire init template install for non-stable CLI builds (microsoft#16654) �spire init ran `dotnet new install Aspire.ProjectTemplates@<cliVersion+sha>` with `nugetConfigFile: null` and `nugetSource: null`, bypassing the channel feed wiring used by `aspire new`. For non-stable CLI builds (staging/daily/PR), `Aspire.ProjectTemplates@<cliVersion+sha>` is only available on a per-commit darc feed (e.g. `darc-pub-microsoft-aspire-<sha8>`), so install failed with exit code 103 in any C# repo containing a `.sln`. Extract the channel-aware template package resolution and install logic out of `DotNetTemplateFactory.ApplyTemplateAsync` and into `TemplateNuGetConfigService` as `ResolveTemplatePackageAsync` and `InstallTemplatePackageAsync`. Both `DotNetTemplateFactory` and `InitCommand` now consume the helper. The existing `aspire new` install path is preserved bit-for-bit (extraction is mechanical; `IncludePrHives: true` keeps PR-hive widening behavior). For `aspire init` this means: - The version sent to `dotnet new install` is now the channel-resolved one (e.g. `13.3.0`), not the raw `+sha` build metadata. - Init now honors the global `channel` configuration, matching `aspire new`. - On install failure, captured stdout/stderr is displayed before the error. - `ChannelNotFoundException` and `EmptyChoicesException` produce friendly errors instead of bubbling to the top-level "unexpected error" handler. - PR hives are intentionally NOT included in init's channel discovery so a developer with stale `~/.aspire/hives/*` doesn't get a different template than they'd get on a clean machine. Notes: - `TemplateNuGetConfigService` is a singleton; `IDotNetCliRunner` is transient and is therefore passed as a method parameter to `InstallTemplatePackageAsync` instead of being injected. - New regression tests cover: explicit channel passes the temp NuGet config, implicit channel leaves it null, PR hives don't widen init, and channel resolution failures produce friendly errors. Fixes microsoft#16654 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback (microsoft#16672) Multi-model code review caught the following issues: 1. Restore original order of operations in DotNetTemplateFactory.ApplyTemplateAsync. The first refactor moved extraArgsCallback ahead of template package resolution, which changed prompt/error precedence for `aspire new` (extra-args prompts like Redis-cache, test-framework, xUnit-version would now run before channel lookup, and answers would be discarded if resolution failed afterward). Restored the BEFORE order from release/13.3: ResolveTemplatePackageAsync first, then extraArgsCallback, then InstallTemplatePackageAsync. Updated the in-source comment to be accurate. 2. Catch NuGetPackageCacheException in InitCommand.DropCSharpProjectSkeletonAsync. The pre-extraction init code went straight to `dotnet new install` and never invoked a NuGet search, so feed search failures (offline, inaccessible feed, etc.) couldn't bubble up. After the extraction init now performs the search and was missing the catch, surfacing the failure as an unhandled "unexpected error". Added the catch with the same friendly-error treatment as ChannelNotFoundException / EmptyChoicesException. 3. Use TemplatingStrings.TemplateInstallationFailed in InitCommand for parity with `aspire new`. The previous ad-hoc string omitted the log file path, making post-mortem diagnosis harder. 4. Added a comment in InstallTemplatePackageAsync clarifying that the temporary NuGet config is intentionally disposed at the end of the install (only `dotnet new install` consumes it; the subsequent `dotnet new <template>` call uses the already-installed template hive and ambient NuGet config). 5. Added regression test InitCommand_WhenChannelTemplateSearchFails_DisplaysFriendlyError covering the new NuGetPackageCacheException catch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove duplicate TemplateNuGetConfigService DI registration Cherry-pick from microsoft#16672 added services.AddSingleton<TemplateNuGetConfigService>() to CliTestHelper.cs, but main already had the same registration from microsoft#16636. Drop the duplicate (the cherry-picked one) so the service is only registered once. Caught by Copilot PR review on microsoft#16690. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Jose Perez Rodriguez <joperezr@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Forward-ports #16672 (the
release/13.3fix) tomain. Fixes #16654 —aspire initwas failing with exit code 103 (Aspire.ProjectTemplates::<version>+<sha> could not be installed, the package does not exist) on any C# repo containing a.sln/.slnxfile when the CLI was a non-stable build (staging/daily/PR).The bug exists on
mainas well asrelease/13.3. #16672 is the parallel PR targeting the release branch (CI green, awaiting review).Root cause
InitCommand.DropCSharpProjectSkeletonAsyncwas running:…with
nugetConfigFile: nullandnugetSource: null, bypassing the channel feed wiring thataspire newuses. For non-stable builds,Aspire.ProjectTemplates@<cliVersion+sha>is published only to a per-commit darc feed (e.g.darc-pub-microsoft-aspire-<sha8>), which was never queried.Fix
Extracted the channel-aware template package resolution and install logic out of
DotNetTemplateFactory.ApplyTemplateAsyncand intoTemplateNuGetConfigService(the existing "NuGet glue for templates" service). Two new methods:ResolveTemplatePackageAsync(TemplatePackageQuery, CancellationToken)— picks the right(NuGetPackage, PackageChannel)fromIPackagingService. Verbatim move of the old privateGetProjectTemplatesVersionAsyncwith one new opt-in flag (IncludePrHives).InstallTemplatePackageAsync(TemplatePackageSelection, IDotNetCliRunner, ...)— generates theTemporaryNuGetConfigfrom the channel mappings (when explicit) and runsdotnet new installwith the right--nuget-sourceand--configfile. Verbatim move of the install block fromApplyTemplateAsync.Both
DotNetTemplateFactory.ApplyTemplateAsyncandInitCommand.DropCSharpProjectSkeletonAsyncnow call these helpers.Behavior preservation for
aspire newextraArgsCallbackstill runs before template install but after channel resolution (preserves prompt/error precedence).IncludePrHives: truekeeps PR-hive widening on foraspire new.KnownEmojis.Icestatus emoji retained foraspire new.DotNetTemplateFactoryandNewCommandtests pass unchanged.Behavior changes for
aspire init(all intentional improvements)dotnet new installis now the channel-resolved version (e.g.13.4.0) instead of<cliVersion+sha>.aspire initnow honors the globalchannelconfiguration setting, matchingaspire new.ChannelNotFoundException/EmptyChoicesException/NuGetPackageCacheExceptionproduce friendly errors (returnsFailedToInstallTemplates) instead of bubbling to the top-level "unexpected error" handler.aspire initdoes NOT include PR hives (IncludePrHives: false) so a developer with stale~/.aspire/hives/*doesn't get a different template than they would on a clean machine.Implementation notes
TemplateNuGetConfigServiceis a singleton;IDotNetCliRunneris transient, so it's passed as a method parameter toInstallTemplatePackageAsyncrather than injected (avoids the singleton-captures-transient lifetime trap).IncludePrHivesopt-in flag, which is an additive change gated toaspire init.Forward-port notes
The two commits from #16672 cherry-picked cleanly onto
mainwith one trivial conflict inTemplateNuGetConfigService.cs:mainalready gainedCreateOrUpdateNuGetConfigWithoutPromptAsyncfrom #16636 (single-fileaspire initnuget.configdrop). Resolved by keeping that method and appending the newResolveTemplatePackageAsync/InstallTemplatePackageAsyncmethods alongside it.InitCommand.cs,NewCommandTests.cs, andCliTestHelper.csauto-merged because the constructor field/registration changes from #16636 happened to be additive.Tests added
In
tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs:InitCommand_WhenSolutionExistsAndChannelIsExplicit_PassesTemporaryNuGetConfigToTemplateInstall— repros the bug scenario (globalchannel = staging) and asserts the temp NuGet config is generated with the channel's mappings and passed toInstallTemplateAsync.InitCommand_WhenSolutionExistsAndChannelIsImplicit_LeavesNuGetConfigNull— asserts no temp NuGet config is generated for the implicit channel.InitCommand_WhenSolutionExistsAndPrHivesPresent_DoesNotWidenToAllChannels— asserts that stale PR hive directories are ignored by init (covers blocking finding from rubber-duck review).InitCommand_WhenChannelResolutionThrowsChannelNotFound_DisplaysFriendlyError— asserts a misconfigured channel surfaces a friendly error rather than an unexpected-error stack trace.InitCommand_WhenChannelTemplateSearchFails_DisplaysFriendlyError— covers theNuGetPackageCacheExceptioncatch.Verification
Built the CLI from this branch and ran the targeted Aspire.Cli.Tests suite locally:
Maddy Montaquila (@maddymontaquila) will verify locally on the original
bitwarden/serverrepro once CI is green.Checklist
TemplateNuGetConfigService).