diff --git a/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs b/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs index 9b9663ba0f4..6abe0441159 100644 --- a/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs +++ b/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs @@ -324,8 +324,8 @@ private XDocument CreateProjectFile(IEnumerable 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)) { diff --git a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs index 5c05e2a66d7..0bc97a6eada 100644 --- a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs +++ b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs @@ -330,13 +330,13 @@ internal static string GenerateIntegrationProjectFile( } /// - /// Resolves the configured channel name from local settings.json or global config. + /// Resolves the configured channel name from local project config or global config. /// private async Task 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; // Fall back to global config if (string.IsNullOrEmpty(channelName)) diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.TypeScriptStarterTemplate.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.TypeScriptStarterTemplate.cs index cdb6fe354c0..0bff27ba9cd 100644 --- a/src/Aspire.Cli/Templating/CliTemplateFactory.TypeScriptStarterTemplate.cs +++ b/src/Aspire.Cli/Templating/CliTemplateFactory.TypeScriptStarterTemplate.cs @@ -61,16 +61,14 @@ private async Task 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; } + config.Save(outputPath); var appHostProject = _projectFactory.TryGetProject(new FileInfo(Path.Combine(outputPath, "apphost.ts"))); if (appHostProject is not IGuestAppHostSdkGenerator guestProject) diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs index 2467e4466f3..1b2f3ed1605 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -1296,6 +1296,7 @@ public async Task NewCommandWithTypeScriptStarterGeneratesSdkArtifacts() var buildAndGenerateCalled = false; string? channelSeenByProject = null; + string? sdkVersionSeenByProject = null; var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => { @@ -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"); @@ -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"))); } diff --git a/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs b/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs index 89ac6a12b2b..7b9d6c4ba34 100644 --- a/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs @@ -237,12 +237,12 @@ public void UserSecretsId_IsStableForSameAppPath() /// /// 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 @@ -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" + } } """); diff --git a/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs b/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs index 8888eff0583..c4dbad8138f 100644 --- a/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs @@ -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.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>(method.Invoke(server, [CancellationToken.None])); + var channel = await channelTask; + + Assert.Equal("pr-new", channel); + } + }