From c5ac6cfac68266fe548da155b9fae04fa3ccda1a Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 20 Mar 2026 12:07:47 +1100 Subject: [PATCH 1/2] Auto-enable staging channel when channel is set to staging Add KnownFeatures.IsStagingChannelEnabled helper that checks both the feature flag and the configured channel. The staging channel is now considered enabled if either features.stagingChannelEnabled is true OR the channel setting is "staging" (case-insensitive). This removes the need to explicitly set features.stagingChannelEnabled when the user already has channel set to staging in their config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Commands/InitCommand.cs | 6 +- src/Aspire.Cli/Commands/NewCommand.cs | 6 +- src/Aspire.Cli/Commands/UpdateCommand.cs | 10 +- src/Aspire.Cli/KnownFeatures.cs | 19 +++ src/Aspire.Cli/Packaging/PackagingService.cs | 2 +- .../StagingChannelTests.cs | 11 +- .../Configuration/KnownFeaturesTests.cs | 109 ++++++++++++++++++ 7 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs index 49a8af6276f..b83f3814977 100644 --- a/src/Aspire.Cli/Commands/InitCommand.cs +++ b/src/Aspire.Cli/Commands/InitCommand.cs @@ -16,6 +16,7 @@ using Aspire.Cli.Telemetry; using Aspire.Cli.Templating; using Aspire.Cli.Utils; +using Microsoft.Extensions.Configuration; using NuGetPackage = Aspire.Shared.NuGetPackageCli; using Semver; using Spectre.Console; @@ -84,7 +85,8 @@ public InitCommand( ILanguageDiscovery languageDiscovery, IScaffoldingService scaffoldingService, AgentInitCommand agentInitCommand, - ICliHostEnvironment hostEnvironment) + ICliHostEnvironment hostEnvironment, + IConfiguration configuration) : base("init", InitCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry) { _runner = runner; @@ -107,7 +109,7 @@ public InitCommand( Options.Add(s_versionOption); // Customize description based on whether staging channel is enabled - var isStagingEnabled = features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false); + var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(features, configuration); _channelOption = new Option("--channel") { Description = isStagingEnabled diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index cc8d4f48c4a..4ccc4564a0a 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -12,6 +12,7 @@ using Aspire.Cli.Telemetry; using Aspire.Cli.Templating; using Aspire.Cli.Utils; +using Microsoft.Extensions.Configuration; using Spectre.Console; using NuGetPackage = Aspire.Shared.NuGetPackageCli; @@ -74,7 +75,8 @@ public NewCommand( IPackagingService packagingService, IConfigurationService configurationService, AgentInitCommand agentInitCommand, - ICliHostEnvironment hostEnvironment) + ICliHostEnvironment hostEnvironment, + IConfiguration configuration) : base("new", NewCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry) { _prompter = prompter; @@ -91,7 +93,7 @@ public NewCommand( Options.Add(s_versionOption); // Customize description based on whether staging channel is enabled - var isStagingEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false); + var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, configuration); _channelOption = new Option("--channel") { Description = isStagingEnabled diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index ca91c32b188..cb80b265f04 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -13,6 +13,7 @@ using Aspire.Cli.Resources; using Aspire.Cli.Telemetry; using Aspire.Cli.Utils; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Spectre.Console; @@ -30,6 +31,7 @@ internal sealed class UpdateCommand : BaseCommand private readonly ICliUpdateNotifier _updateNotifier; private readonly IFeatures _features; private readonly IConfigurationService _configurationService; + private readonly IConfiguration _configuration; private static readonly OptionWithLegacy s_appHostOption = new("--apphost", "--project", UpdateCommandStrings.ProjectArgumentDescription); private static readonly Option s_selfOption = new("--self") @@ -50,7 +52,8 @@ public UpdateCommand( ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, IConfigurationService configurationService, - AspireCliTelemetry telemetry) + AspireCliTelemetry telemetry, + IConfiguration configuration) : base("update", UpdateCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry) { _projectLocator = projectLocator; @@ -61,12 +64,13 @@ public UpdateCommand( _updateNotifier = updateNotifier; _features = features; _configurationService = configurationService; + _configuration = configuration; Options.Add(s_appHostOption); Options.Add(s_selfOption); // Customize description based on whether staging channel is enabled - var isStagingEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false); + var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, _configuration); _channelOption = new Option("--channel") { @@ -270,7 +274,7 @@ private async Task ExecuteSelfUpdateAsync(ParseResult parseResult, Cancella // for future 'aspire new' and 'aspire init' commands. if (string.IsNullOrEmpty(channel)) { - var isStagingEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false); + var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, _configuration); var channels = isStagingEnabled ? new[] { PackageChannelNames.Stable, PackageChannelNames.Staging, PackageChannelNames.Daily } : new[] { PackageChannelNames.Stable, PackageChannelNames.Daily }; diff --git a/src/Aspire.Cli/KnownFeatures.cs b/src/Aspire.Cli/KnownFeatures.cs index f478b7acd27..e3edb4caec9 100644 --- a/src/Aspire.Cli/KnownFeatures.cs +++ b/src/Aspire.Cli/KnownFeatures.cs @@ -1,6 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Cli.Configuration; +using Aspire.Cli.Packaging; +using Microsoft.Extensions.Configuration; + namespace Aspire.Cli; /// @@ -102,4 +106,19 @@ public static IEnumerable GetAllFeatureNames() { return s_featureMetadata.Keys.OrderBy(name => name); } + + /// + /// Determines whether the staging channel is enabled by checking both the feature flag + /// and the configured channel. The staging channel is considered enabled if either the + /// feature flag is true, or the configured + /// channel is set to "staging". + /// + /// The feature flags service. + /// The configuration to check for the channel setting. + /// true if the staging channel should be available; otherwise, false. + public static bool IsStagingChannelEnabled(IFeatures features, IConfiguration configuration) + { + return features.IsFeatureEnabled(StagingChannelEnabled, false) + || string.Equals(configuration["channel"], PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase); + } } diff --git a/src/Aspire.Cli/Packaging/PackagingService.cs b/src/Aspire.Cli/Packaging/PackagingService.cs index d1e91bc06d4..ddb0b6dae7a 100644 --- a/src/Aspire.Cli/Packaging/PackagingService.cs +++ b/src/Aspire.Cli/Packaging/PackagingService.cs @@ -57,7 +57,7 @@ public Task> GetChannelsAsync(CancellationToken canc var channels = new List([defaultChannel, stableChannel]); // Add staging channel if feature is enabled (after stable, before daily) - if (features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false)) + if (KnownFeatures.IsStagingChannelEnabled(features, configuration)) { var stagingChannel = CreateStagingChannel(); if (stagingChannel is not null) diff --git a/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs index 33453092e00..e0b6130740e 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs @@ -34,10 +34,8 @@ public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels() await auto.InstallAspireCliInDockerAsync(installMode, counter); // Step 1: Configure staging channel settings via aspire config set - // Enable the staging channel feature flag - await auto.TypeAsync("aspire config set features.stagingChannelEnabled true -g"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); + // Note: we do NOT need to enable features.stagingChannelEnabled — setting channel + // to staging is sufficient to enable the staging channel behavior. // Set quality to Prerelease (triggers shared feed mode) await auto.TypeAsync("aspire config set overrideStagingQuality Prerelease -g"); @@ -49,7 +47,7 @@ public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels() await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - // Set channel to staging + // Set channel to staging — this alone enables staging channel behavior await auto.TypeAsync("aspire config set channel staging -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); @@ -106,9 +104,6 @@ public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels() await auto.WaitForSuccessPromptAsync(counter); // Clean up: remove staging settings to avoid polluting other tests - await auto.TypeAsync("aspire config delete features.stagingChannelEnabled -g"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); await auto.TypeAsync("aspire config delete overrideStagingQuality -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); diff --git a/tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs b/tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs new file mode 100644 index 00000000000..fe5cbec7157 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Configuration; +using Aspire.Cli.Packaging; +using Microsoft.Extensions.Configuration; + +namespace Aspire.Cli.Tests.Configuration; + +public class KnownFeaturesTests +{ + [Fact] + public void IsStagingChannelEnabled_ReturnsTrue_WhenChannelIsStaging() + { + var features = new TestFeatures(stagingChannelEnabled: false); + var configuration = BuildConfiguration(channel: PackageChannelNames.Staging); + + Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration)); + } + + [Fact] + public void IsStagingChannelEnabled_ReturnsTrue_WhenFeatureFlagIsTrue() + { + var features = new TestFeatures(stagingChannelEnabled: true); + var configuration = BuildConfiguration(channel: PackageChannelNames.Stable); + + Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration)); + } + + [Fact] + public void IsStagingChannelEnabled_ReturnsTrue_WhenChannelIsStagingAndFlagExplicitlyFalse() + { + var features = new TestFeatures(stagingChannelEnabled: false); + var configuration = BuildConfiguration(channel: PackageChannelNames.Staging); + + Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration)); + } + + [Fact] + public void IsStagingChannelEnabled_ReturnsFalse_WhenChannelIsNotStagingAndFlagNotSet() + { + var features = new TestFeatures(stagingChannelEnabled: false); + var configuration = BuildConfiguration(channel: PackageChannelNames.Stable); + + Assert.False(KnownFeatures.IsStagingChannelEnabled(features, configuration)); + } + + [Fact] + public void IsStagingChannelEnabled_ReturnsFalse_WhenChannelIsNullAndFlagNotSet() + { + var features = new TestFeatures(stagingChannelEnabled: false); + var configuration = BuildConfiguration(channel: null); + + Assert.False(KnownFeatures.IsStagingChannelEnabled(features, configuration)); + } + + [Fact] + public void IsStagingChannelEnabled_IsCaseInsensitive_ForChannelValue() + { + var features = new TestFeatures(stagingChannelEnabled: false); + var configuration = BuildConfiguration(channel: "Staging"); + + Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration)); + } + + [Fact] + public void IsStagingChannelEnabled_IsCaseInsensitive_ForUppercaseChannelValue() + { + var features = new TestFeatures(stagingChannelEnabled: false); + var configuration = BuildConfiguration(channel: "STAGING"); + + Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration)); + } + + [Fact] + public void IsStagingChannelEnabled_ReturnsFalse_WhenChannelIsDailyAndFlagNotSet() + { + var features = new TestFeatures(stagingChannelEnabled: false); + var configuration = BuildConfiguration(channel: PackageChannelNames.Daily); + + Assert.False(KnownFeatures.IsStagingChannelEnabled(features, configuration)); + } + + private static IConfiguration BuildConfiguration(string? channel) + { + var configData = new Dictionary(); + + if (channel is not null) + { + configData["channel"] = channel; + } + + return new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + } + + private sealed class TestFeatures(bool stagingChannelEnabled) : IFeatures + { + public bool IsFeatureEnabled(string featureFlag, bool defaultValue) + { + return featureFlag switch + { + "stagingChannelEnabled" => stagingChannelEnabled, + _ => defaultValue + }; + } + } +} From a7ffeb1b12bd7512e9d96b03e03ded0548e4c835 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 24 Mar 2026 09:18:47 +1100 Subject: [PATCH 2/2] Address review feedback: fix duplicate test and use constant - Replace duplicate test with meaningful 'both flag and channel enabled' case - Use KnownFeatures.StagingChannelEnabled instead of hardcoded string in TestFeatures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Configuration/KnownFeaturesTests.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs b/tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs index fe5cbec7157..42661a8fb28 100644 --- a/tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs +++ b/tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs @@ -28,9 +28,9 @@ public void IsStagingChannelEnabled_ReturnsTrue_WhenFeatureFlagIsTrue() } [Fact] - public void IsStagingChannelEnabled_ReturnsTrue_WhenChannelIsStagingAndFlagExplicitlyFalse() + public void IsStagingChannelEnabled_ReturnsTrue_WhenBothChannelIsStagingAndFlagIsTrue() { - var features = new TestFeatures(stagingChannelEnabled: false); + var features = new TestFeatures(stagingChannelEnabled: true); var configuration = BuildConfiguration(channel: PackageChannelNames.Staging); Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration)); @@ -99,11 +99,12 @@ private sealed class TestFeatures(bool stagingChannelEnabled) : IFeatures { public bool IsFeatureEnabled(string featureFlag, bool defaultValue) { - return featureFlag switch + if (featureFlag == KnownFeatures.StagingChannelEnabled) { - "stagingChannelEnabled" => stagingChannelEnabled, - _ => defaultValue - }; + return stagingChannelEnabled; + } + + return defaultValue; } } }