Provide a supported way to skip Aspire CLI bundle resolution for build-only scenarios (CI)
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Is your feature request related to a problem? Please describe the problem.
With `AspireUseCliBundle=true`, every `dotnet build` of an AppHost on a machine with no `aspire` on `PATH` acquires the CLI through `dnx` and unpacks a bundle into `~/.aspire`. That is exactly right on a developer machine, but it is pure overhead in CI, which compiles and tests an AppHost without ever launching orchestration — DCP and the dashboard are never invoked.
Measured on GitHub Actions (`ubuntu-24.04`, .NET SDK `11.0.100-preview.6`, Aspire `13.5.3`), enabling the bundle cost ~10s on every run:
| run | Restore | Build |
| --- | --- | --- |
| `AspireUseCliBundle` unset | 26s | 154s |
| `AspireUseCliBundle` unset | 24s | 155s |
| `AspireUseCliBundle=true` | 24s | **164s** |
Worth noting the acquisition happens inside `dotnet build`, not `dotnet restore` (`ResolveAspireCliBundlePaths` runs via `DependsOnTargets` of the `Set*DiscoveryAttributes` targets, which are `BeforeTargets="GetAssemblyAttributes"`). Two consequences beyond the time:
- **The build step gains a hard network dependency**, even under `dotnet build --no-restore`, which is otherwise hermetic. `aspire.cli` is not in `packages.lock.json`, so neither locked-mode restore nor NuGet package caching covers it — this is the build-only face of #19717.
- **It repeats on every run**, since there is nothing keyed to cache it by.
As far as I can tell there is no supported way to turn this off. The complete set of non-underscore properties across `Aspire.AppHost.Sdk` and `Aspire.Hosting.AppHost` 13.5.3 is:
> `AspireCliBundlePath`, `AspireCliInvocationMode`, `AspireCliPath`, `AspireDashboardDir`, `AspireDashboardPath`, `AspireGeneratedClassesVisibility`, `AspireHostingSDKVersion`, `AspireManifestPublishOutputPath`, `AspirePublisher`, `AspireRidToolDirectory`, `AspireRidToolExecutable`, `AspireRidToolRoot`, `AspireTerminalHostDir`, `AspireTerminalHostInvocationArgs`, `AspireTerminalHostPath`, `AspireUseCliBundle`, `DcpCliPath`, `DcpDir`, `DcpExtensionsDir`, `IsAspireHost`, `SkipAddAspireDefaultReferences`, `SkipValidateAspireHostProjectResources`, `WriteAspireProjectMetadataSourcesDependsOn`
The only two `Skip*` switches are `SkipAddAspireDefaultReferences` and `SkipValidateAspireHostProjectResources`; neither affects bundle resolution.
Setting `AspireUseCliBundle=false` just for CI is not an equivalent escape hatch: it re-adds the `Aspire.Hosting.Orchestration.*` / `Aspire.Dashboard.Sdk.*` package references (so the lock file would have to differ between CI and developer machines) and it re-triggers ASPIRE010, which fails any build using `-warnaserror`.
### Describe the solution you'd like
A first-class opt-out for build-only consumers, e.g.
```xml
true
```
or, equivalently, an `AspireCliInvocationMode=None`.
Semantics: skip `ResolveAspireCliBundlePaths` and the `Set*DiscoveryAttributes` targets, emit neither ASPIRE009 nor ASPIRE010, and let the AppHost compile normally. Anything that then tried to launch orchestration from that output would fail at run time — which is the right trade for a configuration that explicitly declares "I am only building this".
This would also give #19717 a clean answer for the build-only case: an offline machine that only needs to compile could opt out rather than fail.
### Additional context
The workaround we landed on is to pre-supply the paths that the resolution exists to discover, so the target's condition is already false before it runs:
```yml
env:
DcpDir: /nonexistent/aspire-orchestration-unused-in-ci
AspireDashboardDir: /nonexistent/aspire-orchestration-unused-in-ci
```
which works against this condition in `Aspire.Hosting.AppHost.targets`:
```xml
```
Verified in a cold environment (no `aspire` on `PATH`, empty `~/.aspire`, empty NuGet cache): no `dnx` invocation, nothing downloaded, `~/.aspire` never created, and the AppHost still compiles with `-warnaserror`.
It works, but it leans on those properties not being validated, and it bakes non-existent `dcpclipath` / `aspiredashboardpath` values into the assembly as `AssemblyMetadata` — which seems like the sort of thing that could reasonably start being validated in a future release, at which point the workaround breaks. Hence the request for something supported.
Related: #19717 (DNX acquisition offline), #19715 (acquisition failures hard to troubleshoot in CI), #19714 (invalid `AspireCliInvocationMode` values silently ignored).
Contributor guide
Research direction
Start with Aspire.Hosting.AppHost.targets, especially ResolveAspireCliBundlePaths, _ResolveAspireCliInvocation, and the Set*DiscoveryAttributes targets described in the issue. Trace their conditions and property handling, then define a supported build-only opt-out that skips resolution and emits neither ASPIRE009 nor ASPIRE010 while allowing normal compilation. Verify the behavior in a cold CI-like environment with no CLI or cached bundle.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- build-system, ci-cd, cli
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100