Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cf82ee0
fix(cli): honor source for guest language package restore
radical May 16, 2026
a49d604
chore(cli): remove source restore diff noise
radical May 16, 2026
7ef60d4
fix(cli): constrain source override restore behavior
radical May 16, 2026
32d4b3d
Merge origin/main into source restore fix
radical May 18, 2026
0381a4e
Merge remote-tracking branch 'origin/main' into radical/issue-17159-a…
radical May 18, 2026
845de43
Merge remote-tracking branch 'origin/main' into radical/issue-17159-a…
radical May 18, 2026
d60807b
Merge remote-tracking branch 'origin/main' into radical/issue-17159-a…
radical May 18, 2026
9b6d705
fix(cli): keep --source override exclusive for Aspire packages
radical May 19, 2026
c3b0183
fix(cli): warn that aspire-empty --source is one-shot at scaffold
radical May 19, 2026
7518348
fix(cli): include --source/channel context in scaffold restore failures
radical May 19, 2026
e68732a
Merge remote-tracking branch 'origin/main' into radical/issue-17159-a…
radical May 19, 2026
b59e2a0
fix(cli): honor --source override for guest-language starter templates
radical May 19, 2026
1f0c606
chore(cli): rename EmptySourceOverrideNotPersistedWarning resource
radical May 19, 2026
7e76ca4
fix(cli): align --source argument list with temp NuGet.config in Preb…
radical May 19, 2026
6e8bc74
fix(cli): redact credentials from --source in restore-failure output
radical May 19, 2026
f55e41f
fix(cli): auto-discover local Aspire source from requested channel
radical May 19, 2026
7295d17
Merge remote-tracking branch 'origin/main' into radical/issue-17159-a…
radical May 19, 2026
5e069d7
fix(cli): close 5 findings from PR #17166 post-merge review
radical May 19, 2026
057abfa
fix(cli): degrade restore on channel-lookup failure + cover gaps from…
radical May 19, 2026
2b54a5c
Merge origin/main into source restore fix
radical May 19, 2026
968df31
fix(cli): address source-restore review feedback
radical May 19, 2026
d1564d2
fix(cli): persist aspire new source overrides
radical May 19, 2026
ad0f434
Merge remote-tracking branch 'origin/main' into radical/issue-17159-a…
radical May 19, 2026
0bd9fce
fix(cli): reject credentialed new sources before persistence
radical May 19, 2026
94512f5
Merge remote-tracking branch 'origin/main' into radical/issue-17159-a…
radical May 19, 2026
9e82f97
fix(cli): update PR-hive NuGet config snapshots
radical May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,13 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul
{
using var activity = Telemetry.StartDiagnosticActivity(this.Name);

var source = parseResult.GetValue(s_sourceOption);
if (!string.IsNullOrWhiteSpace(source) && PackageSourceOverrideMappings.HasCredentialMaterial(source))
{
InteractionService.DisplayError(NewCommandStrings.SourceWithCredentialsCannotBePersisted);
return CommandResult.Failure(CliExitCodes.InvalidCommand);
}

// Resolve which templates are actually available at runtime (performs
// async checks like SDK availability). This may be a subset of the
// templates registered as subcommands.
Expand Down Expand Up @@ -448,7 +455,7 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul
{
Name = parseResult.GetValue(s_nameOption),
Output = parseResult.GetValue(s_outputOption),
Source = parseResult.GetValue(s_sourceOption),
Source = source,
Version = version,
Channel = parseResult.GetValue(_channelOption) ?? resolvedChannelName,
Language = selectedLanguageId
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Commands/RestoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul

var success = await _interactionService.ShowStatusAsync(
RestoreCommandStrings.RestoringSdkCode,
async () => await configOnlyGuestProject.BuildAndGenerateSdkAsync(configOnlyProjectDirectory, cancellationToken),
async () => await configOnlyGuestProject.BuildAndGenerateSdkAsync(configOnlyProjectDirectory, cancellationToken: cancellationToken),
emoji: KnownEmojis.Gear);

if (success)
Expand Down Expand Up @@ -156,7 +156,7 @@ protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResul

var success = await _interactionService.ShowStatusAsync(
RestoreCommandStrings.RestoringSdkCode,
async () => await guestProject.BuildAndGenerateSdkAsync(directory, cancellationToken),
async () => await guestProject.BuildAndGenerateSdkAsync(directory, cancellationToken: cancellationToken),
emoji: KnownEmojis.Gear);

if (success)
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ private async Task<int> DumpCapabilitiesAsync(
var prepareResult = await appHostServerProject.PrepareAsync(
VersionHelper.GetDefaultTemplateVersion(),
integrations,
cancellationToken);
cancellationToken: cancellationToken);

if (!prepareResult.Success)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ private async Task<int> GenerateSdkAsync(
var prepareResult = await appHostServerProject.PrepareAsync(
VersionHelper.GetDefaultTemplateVersion(),
integrations,
cancellationToken);
cancellationToken: cancellationToken);

if (!prepareResult.Success)
{
Expand Down
28 changes: 27 additions & 1 deletion src/Aspire.Cli/NuGet/BundleNuGetService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,14 @@ public async Task<string> RestorePackagesAsync(

_logger.LogDebug("Restoring {Count} packages", packageList.Count);
_logger.LogDebug("aspire-managed path: {ManagedPath}", managedPath);
_logger.LogDebug("NuGet restore args: {Args}", string.Join(" ", restoreArgs));
if (_logger.IsEnabled(LogLevel.Debug))
{
// Build a redacted copy of the args specifically for the log line so user-supplied
// credentialed feeds (e.g., `https://user:pat@host/v3/index.json`, SAS-token URLs) do
// not flow to the debug log alongside the rest of the restore invocation. The
// original `restoreArgs` list is still passed verbatim to the process below.
_logger.LogDebug("NuGet restore args: {Args}", string.Join(" ", BuildRedactedArgsForLog(restoreArgs)));
}

var environmentVariables = new Dictionary<string, string>();
NuGetSignatureVerificationEnabler.Apply(environmentVariables, _features, _executionContext);
Expand Down Expand Up @@ -253,6 +260,25 @@ private static bool TryValidatePackageManifest(string manifestPath, ILogger logg
}
}

// Returns a redacted copy of the restore args suitable for debug logging. Replaces the value
// immediately following each `--source` token with the credential-safe form from
// PackageSourceRedactor. Built defensively to handle repeated `--source` flags and a missing
// trailing value at the end of the args list.
private static IReadOnlyList<string> BuildRedactedArgsForLog(IReadOnlyList<string> args)
{
var redacted = new List<string>(args.Count);
for (var i = 0; i < args.Count; i++)
{
redacted.Add(args[i]);
if (string.Equals(args[i], "--source", StringComparison.Ordinal) && i + 1 < args.Count)
{
redacted.Add(PackageSourceRedactor.RedactForDisplay(args[++i]));
}
}

return redacted;
}

internal static string ComputePackageHash(
List<(string Id, string Version)> packages,
string tfm,
Expand Down
50 changes: 40 additions & 10 deletions src/Aspire.Cli/Packaging/NuGetConfigMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,45 @@ public static async Task CreateOrUpdateAsync(DirectoryInfo targetDirectory, Pack
return;
}

await CreateOrUpdateAsync(targetDirectory, mappings, channel.ConfigureGlobalPackagesFolder, confirmationCallback, cancellationToken);
}

/// <summary>
/// Creates or updates a NuGet.config file in the specified directory based on the provided package source mappings.
/// </summary>
public static async Task CreateOrUpdateAsync(
DirectoryInfo targetDirectory,
PackageMapping[] mappings,
bool configureGlobalPackagesFolder = false,
Func<FileInfo, XmlDocument?, XmlDocument, CancellationToken, Task<bool>>? confirmationCallback = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(targetDirectory);
ArgumentNullException.ThrowIfNull(mappings);

if (mappings.Length == 0)
{
return;
}

if (!targetDirectory.Exists)
{
targetDirectory.Create();
}

if (!TryFindNuGetConfigInDirectory(targetDirectory, out var nugetConfigFile))
{
await CreateNewNuGetConfigAsync(targetDirectory, channel, confirmationCallback, cancellationToken);
await CreateNewNuGetConfigAsync(targetDirectory, mappings, configureGlobalPackagesFolder, confirmationCallback, cancellationToken);
}
else
{
await UpdateExistingNuGetConfigAsync(nugetConfigFile, channel, confirmationCallback, cancellationToken);
await UpdateExistingNuGetConfigAsync(nugetConfigFile, mappings, configureGlobalPackagesFolder, confirmationCallback, cancellationToken);
}
}

private static async Task CreateNewNuGetConfigAsync(DirectoryInfo targetDirectory, PackageChannel channel, Func<FileInfo, XmlDocument?, XmlDocument, CancellationToken, Task<bool>>? confirmationCallback, CancellationToken cancellationToken)
private static async Task CreateNewNuGetConfigAsync(DirectoryInfo targetDirectory, PackageMapping[] mappings, bool configureGlobalPackagesFolder, Func<FileInfo, XmlDocument?, XmlDocument, CancellationToken, Task<bool>>? confirmationCallback, CancellationToken cancellationToken)
{
var mappings = channel.Mappings;
if (mappings is null || mappings.Length == 0)
if (mappings.Length == 0)
{
return;
}
Expand All @@ -83,7 +103,7 @@ private static async Task CreateNewNuGetConfigAsync(DirectoryInfo targetDirector
}
}

if (channel.ConfigureGlobalPackagesFolder)
if (configureGlobalPackagesFolder)
{
// Need to modify the temporary config to add globalPackagesFolder before copying
await AddGlobalPackagesFolderToConfigAsync(tmpConfig.ConfigFile);
Expand All @@ -92,10 +112,9 @@ private static async Task CreateNewNuGetConfigAsync(DirectoryInfo targetDirector
File.Copy(tmpConfig.ConfigFile.FullName, targetPath, overwrite: true);
}

private static async Task UpdateExistingNuGetConfigAsync(FileInfo nugetConfigFile, PackageChannel channel, Func<FileInfo, XmlDocument?, XmlDocument, CancellationToken, Task<bool>>? confirmationCallback, CancellationToken cancellationToken)
private static async Task UpdateExistingNuGetConfigAsync(FileInfo nugetConfigFile, PackageMapping[] mappings, bool configureGlobalPackagesFolder, Func<FileInfo, XmlDocument?, XmlDocument, CancellationToken, Task<bool>>? confirmationCallback, CancellationToken cancellationToken)
{
var mappings = channel.Mappings;
if (mappings is null || mappings.Length == 0)
if (mappings.Length == 0)
{
return;
}
Expand Down Expand Up @@ -136,7 +155,7 @@ private static async Task UpdateExistingNuGetConfigAsync(FileInfo nugetConfigFil
}
}

if (channel.ConfigureGlobalPackagesFolder)
if (configureGlobalPackagesFolder)
{
AddGlobalPackagesFolderConfiguration(configContext);
}
Expand Down Expand Up @@ -633,6 +652,17 @@ private static void HandleWildcardMappingForExistingSources(
var sourceElement = context.ExistingAdds
.FirstOrDefault(add => string.Equals((string?)add.Attribute("key"), sourceKey, StringComparison.OrdinalIgnoreCase));
var sourceValue = (string?)sourceElement?.Attribute("value");
var isRequiredByCurrentChannel = context.RequiredSources.Contains(sourceKey, StringComparer.OrdinalIgnoreCase) ||
context.RequiredSources.Contains(sourceValue ?? "", StringComparer.OrdinalIgnoreCase);
var requiredSourceHasWildcard = context.Mappings.Any(m =>
m.PackageFilter == PackageMapping.AllPackages &&
(string.Equals(m.Source, sourceKey, StringComparison.OrdinalIgnoreCase) ||
string.Equals(m.Source, sourceValue, StringComparison.OrdinalIgnoreCase)));

if (isRequiredByCurrentChannel && !requiredSourceHasWildcard)
{
continue;
}

// For user-defined sources that still have patterns, also give them wildcard patterns
// to ensure they can serve other packages too. But skip Microsoft-controlled sources
Expand Down
53 changes: 53 additions & 0 deletions src/Aspire.Cli/Packaging/PackageSourceOverrideMappings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Cli.Packaging;

internal static class PackageSourceOverrideMappings
{
internal const string NuGetOrgSource = "https://api.nuget.org/v3/index.json";

public static PackageMapping[] Create(string packageSourceOverride, PackageChannel? requestedChannel)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packageSourceOverride);
if (HasCredentialMaterial(packageSourceOverride))
{
throw new ArgumentException("Credential-bearing HTTP sources cannot be persisted.", nameof(packageSourceOverride));
}

var mappings = new List<PackageMapping>
{
new("Aspire*", packageSourceOverride)
};

if (requestedChannel?.Mappings is not null)
{
foreach (var mapping in requestedChannel.Mappings)
{
if (mapping.PackageFilter.StartsWith("Aspire", StringComparison.OrdinalIgnoreCase))
{
continue;
}

mappings.Add(mapping);
}
}

if (!mappings.Any(static mapping => mapping.PackageFilter == PackageMapping.AllPackages))
{
mappings.Add(new PackageMapping(PackageMapping.AllPackages, NuGetOrgSource));
}

return [.. mappings.DistinctBy(static mapping => $"{mapping.PackageFilter}\0{mapping.Source}")];
}

public static bool HasCredentialMaterial(string source)
{
return Uri.TryCreate(source.Trim(), UriKind.Absolute, out var uri) &&
(uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) &&
(!string.IsNullOrEmpty(uri.UserInfo) ||
!string.IsNullOrEmpty(uri.Query) ||
!string.IsNullOrEmpty(uri.Fragment));
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Projects/AppHostServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ public async Task<AppHostServerSessionResult> CreateAsync(
var appHostServerProject = await _projectFactory.CreateAsync(appHostPath, cancellationToken);

// Prepare the server (create files + build for dev mode, restore packages for prebuilt mode)
var prepareResult = await appHostServerProject.PrepareAsync(sdkVersion, integrations, cancellationToken);
var prepareResult = await appHostServerProject.PrepareAsync(sdkVersion, integrations, cancellationToken: cancellationToken);
if (!prepareResult.Success)
{
return new AppHostServerSessionResult(
Expand Down
26 changes: 21 additions & 5 deletions src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,9 @@ private XDocument CreateProjectFile(IEnumerable<IntegrationReference> integratio
/// </summary>
public async Task<(string ProjectPath, string? ChannelName)> CreateProjectFilesAsync(
IEnumerable<IntegrationReference> integrations,
CancellationToken cancellationToken = default,
string? requestedChannel = null)
string? requestedChannel = null,
string? packageSourceOverride = null,
CancellationToken cancellationToken = default)
{
// Clean obj folder to ensure fresh NuGet restore
var objPath = Path.Combine(_projectModelPath, "obj");
Expand Down Expand Up @@ -353,6 +354,20 @@ private XDocument CreateProjectFile(IEnumerable<IntegrationReference> integratio
}
}

// Thread an explicit `--source` override into the restore sources so the dogfood
// `aspire new --source <pr-hive>` flow is honored in dev mode (in-repo). Prepending
// makes the override the first source NuGet evaluates, which matters when the same
// Aspire package version exists in both the hive and a channel feed. Note: unlike
// PrebuiltAppHostServer this path does not emit Package Source Mappings, so NuGet
// may still consult other sources if the override does not satisfy a request — the
// override is best-effort here, sufficient for the in-repo developer scenario where
// most Aspire.* dependencies come from ProjectReference, not PackageReference.
if (!string.IsNullOrWhiteSpace(packageSourceOverride) &&
!channelSources.Contains(packageSourceOverride, StringComparer.OrdinalIgnoreCase))
{
channelSources.Insert(0, packageSourceOverride);
}

// Create the project file
var doc = CreateProjectFile(integrations);

Expand Down Expand Up @@ -424,10 +439,11 @@ private XDocument CreateProjectFile(IEnumerable<IntegrationReference> integratio
public async Task<AppHostServerPrepareResult> PrepareAsync(
string sdkVersion,
IEnumerable<IntegrationReference> integrations,
CancellationToken cancellationToken = default,
string? requestedChannel = null)
string? requestedChannel = null,
string? packageSourceOverride = null,
CancellationToken cancellationToken = default)
{
var (_, channelName) = await CreateProjectFilesAsync(integrations, cancellationToken, requestedChannel);
var (_, channelName) = await CreateProjectFilesAsync(integrations, requestedChannel, packageSourceOverride, cancellationToken);
var (buildSuccess, buildOutput) = await BuildAsync(cancellationToken);

if (!buildSuccess)
Expand Down
Loading
Loading