Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,8 @@ private XDocument CreateProjectFile(IEnumerable<IntegrationReference> integratio
}

var channels = await _packagingService.GetChannelsAsync(cancellationToken);
var localConfig = AspireJsonConfiguration.Load(_appPath);
var configuredChannelName = localConfig?.Channel;
var configuredChannelName = AspireConfigFile.Load(_appPath)?.Channel
?? AspireJsonConfiguration.Load(_appPath)?.Channel;

if (string.IsNullOrEmpty(configuredChannelName))
{
Expand Down
8 changes: 4 additions & 4 deletions src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,13 @@ internal static string GenerateIntegrationProjectFile(
}

/// <summary>
/// Resolves the configured channel name from local settings.json or global config.
/// Resolves the configured channel name from local project config or global config.
/// </summary>
private async Task<string?> ResolveChannelNameAsync(CancellationToken cancellationToken)
{
// Check local settings.json first
var localConfig = AspireJsonConfiguration.Load(_appDirectoryPath);
var channelName = localConfig?.Channel;
// Check aspire.config.json first, then fall back to legacy .aspire/settings.json.
var channelName = AspireConfigFile.Load(_appDirectoryPath)?.Channel
?? AspireJsonConfiguration.Load(_appDirectoryPath)?.Channel;
Comment on lines +338 to +339
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicated in DotNetBasedAppHostServiceProject. Is there a place we could put a helper method to avoid duplication?


// Fall back to global config
if (string.IsNullOrEmpty(channelName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,14 @@ private async Task<TemplateResult> ApplyTypeScriptStarterTemplateAsync(CallbackT
_logger.LogDebug("Copying embedded TypeScript starter template files to '{OutputPath}'.", outputPath);
await CopyTemplateTreeToDiskAsync("ts-starter", outputPath, ApplyAllTokens, cancellationToken);

// Write channel to aspire.config.json before restore so package resolution uses the selected channel.
// Persist the template SDK version before restore so integration and codegen package
// resolution stays aligned with the project we just created.
var config = AspireConfigFile.LoadOrCreate(outputPath, aspireVersion);
if (!string.IsNullOrEmpty(inputs.Channel))
{
var config = AspireConfigFile.Load(outputPath);
if (config is not null)
{
config.Channel = inputs.Channel;
config.Save(outputPath);
}
config.Channel = inputs.Channel;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Write a log message that channel has been set. Could be the comment above here

}
config.Save(outputPath);

var appHostProject = _projectFactory.TryGetProject(new FileInfo(Path.Combine(outputPath, "apphost.ts")));
if (appHostProject is not IGuestAppHostSdkGenerator guestProject)
Expand Down
3 changes: 3 additions & 0 deletions tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,7 @@ public async Task NewCommandWithTypeScriptStarterGeneratesSdkArtifacts()

var buildAndGenerateCalled = false;
string? channelSeenByProject = null;
string? sdkVersionSeenByProject = null;

var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
Expand Down Expand Up @@ -1344,6 +1345,7 @@ public async Task NewCommandWithTypeScriptStarterGeneratesSdkArtifacts()
buildAndGenerateCalled = true;
var config = AspireConfigFile.Load(directory.FullName);
channelSeenByProject = config?.Channel;
sdkVersionSeenByProject = config?.SdkVersion;

var modulesDir = Directory.CreateDirectory(Path.Combine(directory.FullName, ".modules"));
File.WriteAllText(Path.Combine(modulesDir.FullName, "aspire.ts"), "// generated sdk");
Expand All @@ -1360,6 +1362,7 @@ public async Task NewCommandWithTypeScriptStarterGeneratesSdkArtifacts()
Assert.Equal(0, exitCode);
Assert.True(buildAndGenerateCalled);
Assert.Equal("daily", channelSeenByProject);
Assert.Equal("9.2.0", sdkVersionSeenByProject);
Assert.True(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, ".modules", "aspire.ts")));
}

Expand Down
15 changes: 8 additions & 7 deletions tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,12 @@ public void UserSecretsId_IsStableForSameAppPath()

/// <summary>
/// Regression test for channel switching bug.
/// When a project has a channel configured in .aspire/settings.json (project-local),
/// When a project has a channel configured in aspire.config.json (project-local),
/// the NuGet.config should use that channel's hive path, NOT the global config channel.
///
/// Bug scenario:
/// 1. User runs `aspire update` and selects "pr-new" channel
/// 2. UpdatePackagesAsync saves channel="pr-new" to project-local .aspire/settings.json
/// 2. UpdatePackagesAsync saves channel="pr-new" to project-local aspire.config.json
/// 3. BuildAndGenerateSdkAsync calls CreateProjectFilesAsync
/// 4. BUG: CreateProjectFilesAsync reads channel from GLOBAL config (returns "pr-old")
/// 5. NuGet.config is generated with pr-old hive path instead of pr-new
Expand All @@ -259,14 +259,15 @@ public async Task CreateProjectFiles_NuGetConfig_UsesProjectLocalChannel_NotGlob
var prOldHive = hivesDir.CreateSubdirectory("pr-old");
var prNewHive = hivesDir.CreateSubdirectory("pr-new");

// Create project-local .aspire/settings.json with channel="pr-new"
// Create project-local aspire.config.json with channel="pr-new"
// This simulates what happens after `aspire update` saves the selected channel
var aspireDir = _workspace.WorkspaceRoot.CreateSubdirectory(".aspire");
var settingsJson = Path.Combine(aspireDir.FullName, "settings.json");
await File.WriteAllTextAsync(settingsJson, """
var aspireConfigPath = Path.Combine(_workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName);
await File.WriteAllTextAsync(aspireConfigPath, """
{
"channel": "pr-new",
"sdkVersion": "13.1.0"
"sdk": {
"version": "13.1.0"
}
}
""");

Expand Down
38 changes: 38 additions & 0 deletions tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,42 @@ public void Constructor_UsesUserAspireDirectoryForWorkingDirectory()
}
}

[Fact]
public async Task ResolveChannelNameAsync_UsesProjectLocalAspireConfig_NotGlobalChannel()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);

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

var configurationService = new TestConfigurationService
{
OnGetConfiguration = key => key == "channel" ? "pr-old" : null
};

var nugetService = new BundleNuGetService(new NullLayoutDiscovery(), Microsoft.Extensions.Logging.Abstractions.NullLogger<BundleNuGetService>.Instance);
var server = new PrebuiltAppHostServer(
workspace.WorkspaceRoot.FullName,
"test.sock",
new LayoutConfiguration(),
nugetService,
new TestDotNetCliRunner(),
new TestDotNetSdkInstaller(),
new Aspire.Cli.Tests.Mcp.MockPackagingService(),
configurationService,
Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);

var method = typeof(PrebuiltAppHostServer).GetMethod("ResolveChannelNameAsync", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
Assert.NotNull(method);

var channelTask = Assert.IsType<Task<string?>>(method.Invoke(server, [CancellationToken.None]));
var channel = await channelTask;
Comment on lines +224 to +228
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why reflection and not make the method internal?

There is also reflection in Constructor_UsesUserAspireDirectoryForWorkingDirectory test


Assert.Equal("pr-new", channel);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change

}
Loading