Skip to content

Support default NuGet sources in Aspire configuration - #19392

Draft
Adam Ratzman (adamint) wants to merge 40 commits into
microsoft:mainfrom
adamint:adamint/issue-19340-cli-nuget-source
Draft

Support default NuGet sources in Aspire configuration#19392
Adam Ratzman (adamint) wants to merge 40 commits into
microsoft:mainfrom
adamint:adamint/issue-19340-cli-nuget-source

Conversation

@adamint

@adamint Adam Ratzman (adamint) commented Aug 14, 2026

Copy link
Copy Markdown
Member

Description

--source works for one invocation, but enterprise users need the same approved NuGet source applied by later CLI commands and editor integrations.

This adds a persistent nugetSource setting to local and global Aspire configuration. It is used by:

  • aspire new
  • aspire add
  • aspire integration list
  • aspire integration search

Explicit --source still wins. Relative sources resolve from the configuration file that declares them, including nested workspaces, out-of-tree AppHosts, and global settings.

For aspire add, explicit sources retain their existing force-override behavior. Configured sources use NuGet's effective source list: when the source is already enabled, Aspire preserves ambient fallback sources and package-source mappings; when it is disabled, rebound, removed, or otherwise unavailable, Aspire passes it directly to package installation.

The setting is included in the modern and legacy configuration models, all committed editor schemas, CLI documentation, template prefetch behavior, integration discovery, and guest AppHost SDK generation. Additional credential and filesystem-source policy is tracked separately by #19396.

Usage

aspire config set nugetSource https://packagefeedproxy.microsoft.io/nuget/v3/index.json

Use --global to make it the default outside the current workspace:

aspire config set --global nugetSource https://packagefeedproxy.microsoft.io/nuget/v3/index.json

Fixes #19340

Validation

  • Full Aspire.Cli.Tests: 5,056 total; 5,022 passed; 34 platform skips; 0 failed
  • Full serial repository pack: 0 warnings, 0 errors
  • Playwright CLI proof at 4500a8d570541d7f573a4be746aff9dc7dd6f3ff: 84 passed, 0 failed, 4 explicitly omitted
    • configured, explicit, disabled, rebound, and removed source routing
    • real aspire new and aspire add workflows against local feeds
    • nested, global, and out-of-tree relative-source origins
    • macOS filesystem aliases, NuGet E/EM/EO/EMO tokens, and source-casing regressions
    • the four omitted assertions require a live HTTPS NuGet v3 service; the exact comparisons pass focused tests
  • Multiple independent architecture, regression, Aspire-domain, and real-path review passes; all confirmed findings were fixed and re-reviewed

Playwright proof

Final Playwright CLI proof showing 84 passes, 0 failures, and 4 explicitly omitted HTTPS assertions

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

Adam Ratzman and others added 25 commits August 14, 2026 05:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Apply NuGet-aware package matching, secure extended-drive handling, and selecting-config precedence for out-of-tree AppHosts. Narrow credential guidance to supported provider authentication and refresh localization resources.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Repair malformed configuration before bootstrap, preserve source origin semantics, validate persistent sources, and keep explicit source compatibility. Add focused CLI regressions for templates, path safety, package IDs, and out-of-tree AppHosts.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Normalize configuration in memory during bootstrap, keep unsafe sources ephemeral, and complete source-aware template restores.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Repair literal configuration keys, make source-backed template restores explicit, and carry configured-source routing policy through guest AppHost restores.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Carry required source routing policy through template, scaffolding, guest, and restore paths; enforce deterministic source-backed template restore and temporary-config cleanup.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Use the restore option names exposed by each Aspire template and verify them through the actual template engine.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Keep the no-restore description in template.json without adding an untranslated placeholder to protected localization files.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Terminate template-engine subprocesses when the real alias regression test exceeds the repository test timeout.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Merge disjoint duplicate objects during bootstrap normalization while retaining last-value-wins for type conflicts, and fully reap timed-out template test processes.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Integrate the latest unrelated playground dependency security update before opening the pull request.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Adam Ratzman added 3 commits August 14, 2026 14:08
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Adam Ratzman added 6 commits August 14, 2026 15:23
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Copilot AI review requested due to automatic review settings August 14, 2026 21:15

Copilot AI left a comment

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.

Review details

Suppressed comments (7)

src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs:149

  • When no AppHost is found, this returns the raw IConfiguration value and later resolves it against the invocation working directory. A relative source declared in a parent workspace or the global settings file therefore points at the wrong directory, contrary to the origin-aware behavior used when an AppHost is present. Resolve the value using GetConfigurationFromDirectoryWithOriginAsync before returning it.
            return (
                executionContext.WorkingDirectory,
                ConfiguredChannel: null,
                ConfiguredSource: NormalizeSource(invocationConfiguredSource),
                LanguageId: null,

src/Aspire.Cli/Configuration/ConfigurationService.cs:401

  • For legacy .aspire/settings.json, the declaring directory is .aspire, but this records the workspace root as the origin. Consequently a relative nugetSource is resolved one directory too high for read-only/non-migrating command paths. Use the legacy settings file's actual directory as BaseDirectory.
                return Task.FromResult<ConfigurationValueWithOrigin?>(new(legacySettingsValue, searchDirectory, IsGlobal: false));

src/Aspire.Cli/Configuration/AspireConfigFile.cs:425

  • This copies a relative legacy nugetSource unchanged while moving it from .aspire/settings.json to the parent aspire.config.json, changing the directory it resolves against. The migration already rebases appHostPath for this same layout change; nugetSource needs equivalent rebasing so migration does not silently redirect the feed.
            config.NuGetSource = settings.NuGetSource;

src/Aspire.Cli/Packaging/PackageSourceOverrideMappings.cs:47

  • This case-insensitive fast path bypasses the platform-specific path comparer below. On Linux, distinct enabled/configured feeds such as /feeds/Release and /feeds/release are treated as the same source, causing aspire add to drop the configured override and install from the wrong ambient path. Keep only an ordinal exact-match fast path.
        if (string.Equals(left, right, StringComparison.OrdinalIgnoreCase))

src/Aspire.Cli/Packaging/PackageSourceOverrideMappings.cs:65

  • The URI comparison makes PathAndQuery case-insensitive, although HTTP path and query components can be case-sensitive. Two feeds on the same host that differ only by path casing are therefore considered enabled-equivalent, so the configured override is removed and package installation can hit a different endpoint. Compare only scheme/server case-insensitively and compare path/query ordinally.
                    leftUri,
                    rightUri,
                    UriComponents.SchemeAndServer | UriComponents.PathAndQuery,
                    UriFormat.Unescaped,
                    StringComparison.OrdinalIgnoreCase) == 0;

src/Aspire.Cli/Projects/GuestAppHostProject.cs:1373

  • This always forwards context.Source as a force override, even when it came from configuration (IsSourceExplicit == false). PrebuiltAppHostServer treats any non-null override as an exclusive temporary mapping, so guest AppHosts do not preserve ambient fallback sources/package-source mappings when the configured source is already enabled, unlike the documented aspire add behavior and the .NET project path. Apply the same configured-vs-explicit effective-source decision before PrepareAsync.
        var regenerateSuccess = await BuildAndGenerateSdkAsync(directory, config, context.Source, cancellationToken);

src/Aspire.Cli/Commands/NewCommand.cs:503

  • This now places inherited local/global settings into the same source variable used for an explicit --source, and that value is later assigned to TemplateInputs.Source. Every template factory persists TemplateInputs.Source through CreateOrUpdateNuGetConfigForSourceOverrideAsync, so inherited sources are copied into generated nuget.config files; #19396 explicitly says this work must not do that because local paths become machine-specific and proxy rotation is blocked. Keep the configured source for discovery/installation, but distinguish it from an explicit source before generated-project persistence.
        var source = await GetResolvedEffectiveSourceAsync(parseResult, cancellationToken);
  • Files reviewed: 34/34 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@adamint
Adam Ratzman (adamint) marked this pull request as draft August 14, 2026 21:35
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Copilot AI review requested due to automatic review settings August 14, 2026 21:37

Copilot AI left a comment

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.

Review details

Suppressed comments (3)

src/Aspire.Cli/Packaging/PackageSourceOverrideMappings.cs:65

  • HTTP path and query components are not generally case-insensitive. This makes sources such as https://feed.example/NuGet and https://feed.example/nuget match, causing the configured override to be removed even though they can route to different repositories. Compare only the scheme/server case-insensitively and compare PathAndQuery ordinally.
                    StringComparison.OrdinalIgnoreCase) == 0;

src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs:148

  • When no AppHost is found, this returns the raw startup value and later resolves it against the invocation directory. A relative nugetSource declared in a parent workspace or global settings therefore points at the wrong directory for integration list/search, contrary to the origin-based behavior used when an AppHost exists. Resolve the value using its declaring directory before returning this branch.
                ConfiguredSource: NormalizeSource(invocationConfiguredSource),

src/Aspire.Cli/Commands/NewCommand.cs:503

  • This effective source is forwarded as TemplateInputs.Source, and the existing template factories persist every non-null source into the generated project's NuGet.config. Consequently a source inherited from global or parent configuration is copied into each new project, including machine-specific absolute paths. Issue #19396 explicitly says this change must not copy inherited sources because doing so prevents later proxy rotation; track whether the source was explicit and only persist explicit overrides while still using configured sources for discovery/install/restore.
        var source = await GetResolvedEffectiveSourceAsync(parseResult, cancellationToken);
  • Files reviewed: 34/34 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/Aspire.Cli/Packaging/PackageSourceOverrideMappings.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa

# Conflicts:
#	src/Aspire.Cli/Packaging/PackageChannel.cs
#	tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs
#	tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs
Copilot AI review requested due to automatic review settings August 14, 2026 21:59

Copilot AI left a comment

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.

Review details

Suppressed comments (4)

src/Aspire.Cli/Packaging/PackageSourceOverrideMappings.cs:47

  • This case-insensitive fast path runs before the source kind and platform are known. On Linux, /Feed and /feed can be distinct local sources; treating them as equal makes aspire add drop the configured override and install from the wrong or unavailable feed. Use an exact fast path and let the platform-specific path comparison below handle casing.
        if (string.Equals(left, right, StringComparison.OrdinalIgnoreCase))

src/Aspire.Cli/Packaging/PackageSourceOverrideMappings.cs:65

  • This folds the entire HTTP path and query even though only the scheme and host are case-insensitive. Distinct feeds such as /TenantA/v3/index.json and /tenanta/v3/index.json can therefore match, causing aspire add to omit the configured source and use a different ambient feed. Compare SchemeAndServer case-insensitively and PathAndQuery ordinally.
                    UriComponents.SchemeAndServer | UriComponents.PathAndQuery,
                    UriFormat.Unescaped,
                    StringComparison.OrdinalIgnoreCase) == 0;

src/Aspire.Cli/Projects/GuestAppHostProject.cs:1373

  • This forwards every configured source as an override for guest AppHosts. PrebuiltAppHostServer treats any non-null override like explicit --source and creates a <clear/> temporary NuGet.config, so an already-enabled configured source loses ambient fallback sources and package-source mappings. Mirror the effective-source check used by DotNetAppHostProject so only explicit, disabled, rebound, or unavailable sources are forwarded.
        var regenerateSuccess = await BuildAndGenerateSdkAsync(directory, config, context.Source, cancellationToken);

src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs:148

  • When integration list/search runs without an AppHost, this returns the startup value without its declaring directory. A relative source from a parent workspace or global settings is then resolved against the invocation directory in GetSearchChannelsAsync, contrary to the configuration-file-relative contract. Resolve the value through IConfigurationService in this branch as well.
                ConfiguredSource: NormalizeSource(invocationConfiguredSource),
  • Files reviewed: 34/34 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

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

Copilot-Session: 1f541574-f58e-4d6d-8b84-16927f933faa
Copilot AI review requested due to automatic review settings August 14, 2026 23:04

Copilot AI left a comment

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.

Review details

Suppressed comments (2)

src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs:148

  • When no AppHost is selected, this returns the raw IConfiguration value and GetSearchChannelsAsync later resolves it against the invocation directory. A relative nugetSource declared in an ancestor or global config therefore points at the wrong directory for aspire integration list/search—the AppHost-present path correctly uses BaseDirectory. Resolve the origin here too, and add a no-AppHost nested-directory regression case.
                ConfiguredSource: NormalizeSource(invocationConfiguredSource),

src/Aspire.Cli/Commands/NewCommand.cs:503

  • This collapses an inherited configured source and an explicit --source into the same value; downstream TemplateInputs.Source then calls CreateOrUpdateNuGetConfigForSourceOverrideAsync, so aspire new copies the inherited source into the generated project's NuGet.config. The linked #19396 explicitly says this work must not automatically copy inherited sources because absolute local paths become machine-specific and proxies may rotate. Keep the configured source for discovery/install/initial restore, but only persist it when --source was explicit.
        var source = await GetResolvedEffectiveSourceAsync(parseResult, cancellationToken);
  • Files reviewed: 34/34 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aspire configuration: support a default NuGet source

2 participants