Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 4 additions & 9 deletions src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,10 @@ private async Task<AppHostServerClosureManifest> BuildIntegrationClosureManifest
var restoreDir = Path.Combine(_workingDirectory, "integration-restore");
Directory.CreateDirectory(restoreDir);

// Only synthesize a temp NuGet.config (replacing nuget.config discovery via
// RestoreConfigFile) when an explicit --source or auto-discovered local channel source
// is in play. The explicit-channel-no-override path keeps the user's ambient
// nuget.config in place and contributes channel mappings additively via
// RestoreAdditionalProjectSources so private/internal feeds the user has configured
// remain reachable for non-Aspire transitives during project-ref restore.
using var temporaryNuGetConfig = !string.IsNullOrWhiteSpace(packageSourceOverride)
? await TryCreateTemporaryNuGetConfigAsync(requestedChannel, packageSourceOverride, cancellationToken)
: null;
// Explicit channels need their package source mappings during project-reference restore.
// RestoreAdditionalProjectSources can add the feed URLs, but it cannot override ambient
// packageSourceMapping entries that might route Aspire* packages to another feed.
using var temporaryNuGetConfig = await TryCreateTemporaryNuGetConfigAsync(requestedChannel, packageSourceOverride, cancellationToken);
Comment on lines +322 to +325
var channelSources = temporaryNuGetConfig is null
? await GetNuGetSourcesAsync(requestedChannel, packageSourceOverride: null, cancellationToken)
: null;
Expand Down
49 changes: 32 additions & 17 deletions tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1614,21 +1614,22 @@ public async Task PrepareAsync_RestoreFailure_WithManyPackages_TruncatesPackageL
}

[Fact]
public async Task PrepareAsync_WithProjectReferencesAndExplicitChannelButNoOverride_UsesAdditionalSourcesNotRestoreConfigFile()
public async Task PrepareAsync_WithProjectReferencesAndExplicitChannelButNoOverride_UsesNuGetConfigForChannelMappings()
{
// Regression for finding #1 of the 2026-05-19 post-merge review: a project-ref restore
// with an explicit channel pin (daily/staging/pr-*) and NO --source must not replace the
// user's ambient nuget.config via <RestoreConfigFile>. The channel sources flow through
// additively via <RestoreAdditionalProjectSources> so private/internal feeds the user
// has configured in nuget.config remain reachable for non-Aspire transitives.
// Regression for https://github.com/microsoft/aspire/issues/17629: a project-ref restore
// with an explicit channel pin and no --source needs the channel's package source mappings,
// not only its feed URLs, otherwise ambient packageSourceMapping can route Aspire* packages
// away from the staging/DARC feed.
using var workspace = TemporaryWorkspace.Create(outputHelper);
const string channelSource = "https://pkgs.dev.azure.com/fake/v3/index.json";
const string channelSource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json";
XDocument? generatedProject = null;
XDocument? generatedRestoreConfig = null;
bool restoreConfigFileExistedDuringBuild = false;

var aspireConfigPath = Path.Combine(workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName);
await File.WriteAllTextAsync(aspireConfigPath, """
{
"channel": "daily"
"channel": "staging"
}
""");

Expand All @@ -1641,20 +1642,32 @@ await File.WriteAllTextAsync(aspireConfigPath, """
BuildAsyncCallback = (projectFilePath, _, _, _) =>
{
generatedProject = XDocument.Load(projectFilePath.FullName);
var ns = generatedProject.Root!.GetDefaultNamespace();
var restoreConfigFile = generatedProject.Descendants(ns + "RestoreConfigFile").FirstOrDefault()?.Value;
restoreConfigFileExistedDuringBuild = restoreConfigFile is not null && File.Exists(restoreConfigFile);
if (restoreConfigFile is not null)
{
generatedRestoreConfig = XDocument.Load(restoreConfigFile);
}

WriteClosureInputs(projectFilePath.Directory!, closureFiles, ["MyIntegration"]);
return 0;
}
};

var dailyChannel = PackageChannel.CreateExplicitChannel(
name: "daily",
var stagingChannel = PackageChannel.CreateExplicitChannel(
name: "staging",
quality: PackageChannelQuality.Both,
mappings: [new PackageMapping("Aspire*", channelSource)],
mappings:
[
new PackageMapping("Aspire*", channelSource),
new PackageMapping(PackageMapping.AllPackages, NuGetOrgSource)
],
nuGetPackageCache: new FakeNuGetPackageCache(),
features: new TestFeatures());
var packagingService = new TestPackagingService
{
GetChannelsAsyncCallback = _ => Task.FromResult<IEnumerable<PackageChannel>>([dailyChannel])
GetChannelsAsyncCallback = _ => Task.FromResult<IEnumerable<PackageChannel>>([stagingChannel])
};

var nugetService = new BundleNuGetService(
Expand Down Expand Up @@ -1688,11 +1701,13 @@ await File.WriteAllTextAsync(aspireConfigPath, """
Assert.NotNull(generatedProject);

var ns = generatedProject!.Root!.GetDefaultNamespace();
Assert.Null(generatedProject.Descendants(ns + "RestoreConfigFile").FirstOrDefault());

var restoreSources = generatedProject.Descendants(ns + "RestoreAdditionalProjectSources").FirstOrDefault()?.Value;
Assert.NotNull(restoreSources);
Assert.Contains(channelSource, restoreSources!);
var restoreConfigFile = generatedProject.Descendants(ns + "RestoreConfigFile").FirstOrDefault()?.Value;
Assert.NotNull(restoreConfigFile);
Assert.True(restoreConfigFileExistedDuringBuild);
Assert.NotNull(generatedRestoreConfig);
Assert.Equal(["Aspire*"], GetPackagePatternsForSource(generatedRestoreConfig!, channelSource));
Assert.Equal([PackageMapping.AllPackages], GetPackagePatternsForSource(generatedRestoreConfig!, NuGetOrgSource));
Assert.Null(generatedProject.Descendants(ns + "RestoreAdditionalProjectSources").FirstOrDefault());

// Aspire package versions remain in their original (non-pinned) form when no override
// is in play; the exact-version pinning only fires when a single source is selected.
Expand Down
Loading