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..42661a8fb28 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs @@ -0,0 +1,110 @@ +// 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_WhenBothChannelIsStagingAndFlagIsTrue() + { + var features = new TestFeatures(stagingChannelEnabled: true); + 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) + { + if (featureFlag == KnownFeatures.StagingChannelEnabled) + { + return stagingChannelEnabled; + } + + return defaultValue; + } + } +}