diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index e815a74d6f1..7c03ae5b758 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -184,7 +184,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell { // Try to find a channel matching the provided channel/quality channel = allChannels.FirstOrDefault(c => string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase)) - ?? throw new ChannelNotFoundException($"No channel found matching '{channelName}'. Valid options are: {string.Join(", ", allChannels.Select(c => c.Name))}"); + ?? throw new ChannelNotFoundException(BuildChannelNotFoundMessage(channelName, allChannels)); if (channelFromConfig) { @@ -304,6 +304,24 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell return 0; } + private string BuildChannelNotFoundMessage(string channelName, IEnumerable allChannels) + { + // For an explicit `--channel staging` request, surface the staging-specific + // unavailability reason (issue #16652) instead of the generic "no channel + // matching" message so users on a daily CLI know why the channel was omitted + // and how to recover (set 'overrideStagingFeed' or use a stable CLI). + if (string.Equals(channelName, PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase)) + { + var stagingReason = _packagingService.GetStagingChannelUnavailableReason(); + if (!string.IsNullOrEmpty(stagingReason)) + { + return stagingReason; + } + } + + return $"No channel found matching '{channelName}'. Valid options are: {string.Join(", ", allChannels.Select(c => c.Name))}"; + } + private async Task ExecuteSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken, string? selectedChannel = null) { var channel = selectedChannel ?? parseResult.GetValue(_channelOption) ?? parseResult.GetValue(_qualityOption); diff --git a/src/Aspire.Cli/Packaging/PackagingService.cs b/src/Aspire.Cli/Packaging/PackagingService.cs index f3121672647..e2860692f14 100644 --- a/src/Aspire.Cli/Packaging/PackagingService.cs +++ b/src/Aspire.Cli/Packaging/PackagingService.cs @@ -3,17 +3,42 @@ using Aspire.Cli.Configuration; using Aspire.Cli.NuGet; +using Aspire.Cli.Resources; using Aspire.Cli.Utils; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Semver; -using System.Reflection; +using System.Globalization; namespace Aspire.Cli.Packaging; internal interface IPackagingService { public Task> GetChannelsAsync(CancellationToken cancellationToken = default); + + /// + /// When the running CLI cannot deterministically resolve the staging channel, + /// returns a localized, user-facing explanation of why. Returns + /// when the staging channel can be created (or when the staging channel is not enabled + /// at all). Callers that observe a missing staging channel should consult this + /// to produce a clearer error message. + /// + public string? GetStagingChannelUnavailableReason(); +} + +/// +/// Internal configuration keys consumed by . +/// +internal static class PackagingConfigurationKeys +{ + /// + /// Test-only override of the CLI assembly informational version used when deciding + /// whether the running CLI is a daily/CI build. Production callers should never set + /// this value; it exists so unit tests can deterministically simulate stable, blessed + /// preview/RC, and daily CLI builds without depending on the actual assembly version + /// of Aspire.Cli.dll at test time. + /// + public const string CliVersionForTesting = "internal:packaging:cliVersionForTesting"; } internal class PackagingService(CliExecutionContext executionContext, INuGetPackageCache nuGetPackageCache, IFeatures features, IConfiguration configuration, ILogger logger) : IPackagingService @@ -79,8 +104,55 @@ public Task> GetChannelsAsync(CancellationToken canc return Task.FromResult>(channels); } + public string? GetStagingChannelUnavailableReason() + { + // The 'staging' channel is only meaningful when the feature/channel-config + // explicitly enabled it. If it isn't enabled, there's no "unavailability" + // to report — the channel simply isn't created. + if (!KnownFeatures.IsStagingChannelEnabled(features, configuration)) + { + return null; + } + + // If the user has supplied an explicit staging feed override they are taking + // ownership of where staging packages come from, so any CLI build is allowed. + var hasExplicitFeedOverride = !string.IsNullOrEmpty(configuration["overrideStagingFeed"]); + if (hasExplicitFeedOverride) + { + return null; + } + + // When the running CLI is itself a daily/CI build, there is no deterministic + // way to derive a real staging feed: a SHA-specific darc-pub-* feed is not + // created for daily commits, and falling back to the shared dotnet9 feed would + // resolve to daily packages — which is the bug tracked by #16652. Refuse to + // synthesize a staging channel in that case so the caller fails fast with an + // actionable error message instead of silently downgrading to daily packages. + var cliVersion = GetCliInformationalVersionForStagingDecision(); + if (IsCliPrereleaseDailyBuild(cliVersion)) + { + return string.Format( + CultureInfo.CurrentCulture, + PackagingStrings.StagingChannelUnavailableForDailyCliFormat, + cliVersion ?? "unknown"); + } + + return null; + } + private PackageChannel? CreateStagingChannel() { + var unavailableReason = GetStagingChannelUnavailableReason(); + if (unavailableReason is not null) + { + // Logged once per channel enumeration so users can see why staging was + // omitted without inspecting source. UpdateCommand surfaces the same + // message via ChannelNotFoundException when the user explicitly passes + // --channel staging. + logger.LogWarning("{UnavailableReason}", unavailableReason); + return null; + } + var stagingQuality = GetStagingQuality(); var hasExplicitFeedOverride = !string.IsNullOrEmpty(configuration["overrideStagingFeed"]); @@ -105,9 +177,95 @@ public Task> GetChannelsAsync(CancellationToken canc new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json") }, nuGetPackageCache, configureGlobalPackagesFolder: !useSharedFeed, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/rc/daily", pinnedVersion: pinnedVersion, logger: logger); + // Surface the resolved feed so users can verify channel resolution from the + // CLI logs without disclosing any embedded credentials or signed parameters. + logger.LogInformation( + "Resolved 'staging' channel: feed='{StagingFeedUrl}', quality='{Quality}', pinnedVersion='{PinnedVersion}'.", + GetRedactedFeedUrlForLogging(stagingFeedUrl), + stagingQuality, + pinnedVersion ?? "(none)"); + return stagingChannel; } + private static string GetRedactedFeedUrlForLogging(string feedUrl) + { + if (!Uri.TryCreate(feedUrl, UriKind.Absolute, out var uri)) + { + return "(invalid or non-standard feed URL)"; + } + + var builder = new UriBuilder(uri) + { + UserName = string.Empty, + Password = string.Empty, + Query = string.Empty, + Fragment = string.Empty + }; + + return builder.Uri.AbsoluteUri; + } + + /// + /// Returns the CLI informational version string used by staging-channel decisions. + /// Honors the test-only configuration override so tests can deterministically + /// simulate stable / blessed-prerelease / daily CLI builds. + /// + private string? GetCliInformationalVersionForStagingDecision() + { + var testOverride = configuration[PackagingConfigurationKeys.CliVersionForTesting]; + if (!string.IsNullOrWhiteSpace(testOverride)) + { + return testOverride; + } + + try + { + return VersionHelper.GetDefaultTemplateVersion(); + } + catch (InvalidOperationException) + { + // Cannot determine assembly version; treat as unknown so callers can decide. + return null; + } + } + + /// + /// Determines whether the supplied informational version represents a daily/CI + /// build of the Aspire CLI as opposed to either a stable release or a blessed + /// preview/RC build that has its own staging feed. + /// Heuristic: stable releases have no prerelease label; blessed preview/RC builds + /// use simple labels such as preview.1 or rc.1 (≤ 2 prerelease + /// identifiers). Daily/CI builds add date+revision suffixes from Arcade + /// (e.g. preview.1.26210.1, ≥ 3 identifiers), making them easy to distinguish. + /// + internal static bool IsCliPrereleaseDailyBuild(string? informationalVersion) + { + if (string.IsNullOrWhiteSpace(informationalVersion)) + { + // If we cannot determine the version, err on the side of safety and treat + // it as a daily build so we don't silently resolve to daily packages. + return true; + } + + var withoutBuildMetadata = informationalVersion.Split('+')[0]; + if (!SemVersion.TryParse(withoutBuildMetadata, SemVersionStyles.Strict, out var sv)) + { + // Unparseable version — also err on the side of safety. + return true; + } + + if (!sv.IsPrerelease) + { + // Stable release. + return false; + } + + // Blessed prereleases (preview.1, rc.1, etc.) have at most 2 dot-separated + // identifiers. Daily builds add a date + revision suffix giving 3+ identifiers. + return sv.PrereleaseIdentifiers.Count > 2; + } + private string? GetStagingFeedUrl(bool useSharedFeed) { // Check for configuration override first @@ -130,11 +288,9 @@ public Task> GetChannelsAsync(CancellationToken canc // Extract commit hash from assembly version to build staging feed URL // Staging feed URL template: https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-{commitHash}/nuget/v3/index.json - var assembly = Assembly.GetExecutingAssembly(); - var informationalVersion = assembly - .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) - .OfType() - .FirstOrDefault()?.InformationalVersion; + // Honors the test seam so unit tests can deterministically simulate stable + // CLI builds with a known commit hash. + var informationalVersion = GetCliInformationalVersionForStagingDecision(); if (informationalVersion is null) { diff --git a/src/Aspire.Cli/Resources/PackagingStrings.Designer.cs b/src/Aspire.Cli/Resources/PackagingStrings.Designer.cs index 0d28888627e..809626cac9a 100644 --- a/src/Aspire.Cli/Resources/PackagingStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/PackagingStrings.Designer.cs @@ -64,5 +64,14 @@ internal static string BasedOnNuGetConfig { return ResourceManager.GetString("BasedOnNuGetConfig", resourceCulture); } } + + /// + /// Looks up a localized string similar to The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + /// + internal static string StagingChannelUnavailableForDailyCliFormat { + get { + return ResourceManager.GetString("StagingChannelUnavailableForDailyCliFormat", resourceCulture); + } + } } } diff --git a/src/Aspire.Cli/Resources/PackagingStrings.resx b/src/Aspire.Cli/Resources/PackagingStrings.resx index bd670970a4b..b5b564985f5 100644 --- a/src/Aspire.Cli/Resources/PackagingStrings.resx +++ b/src/Aspire.Cli/Resources/PackagingStrings.resx @@ -121,4 +121,8 @@ based on NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.cs.xlf index 47bc6a1a595..53504c3f437 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.cs.xlf @@ -7,6 +7,11 @@ na základě souboru NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.de.xlf index 516a7b4733e..b9794717469 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.de.xlf @@ -7,6 +7,11 @@ basierend auf NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.es.xlf index fbcca0783de..e5619f21384 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.es.xlf @@ -7,6 +7,11 @@ basado en NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.fr.xlf index 4269afb3126..0587ecebe93 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.fr.xlf @@ -7,6 +7,11 @@ basé sur NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.it.xlf index 0aaa8ae6d24..06142ae2bca 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.it.xlf @@ -7,6 +7,11 @@ basato su NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.ja.xlf index 3f27a8ca92d..6b564595baa 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.ja.xlf @@ -7,6 +7,11 @@ NuGet.config に基づきます Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.ko.xlf index 7a97283c4c9..a3b4eb0a7af 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.ko.xlf @@ -7,6 +7,11 @@ NuGet.config 기반 Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.pl.xlf index ec088b451c3..a3a36353cf1 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.pl.xlf @@ -7,6 +7,11 @@ na podstawie pliku NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.pt-BR.xlf index f2571b291dd..fe55143d468 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.pt-BR.xlf @@ -7,6 +7,11 @@ com base em NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.ru.xlf index 6cd91a4ebf5..7e2148393e6 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.ru.xlf @@ -7,6 +7,11 @@ на основе NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.tr.xlf index 5bf4d67b833..adcf3392044 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.tr.xlf @@ -7,6 +7,11 @@ NuGet.config tabanlı Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hans.xlf index e9d75904565..04640beca35 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hans.xlf @@ -7,6 +7,11 @@ 基于 NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hant.xlf index 28db6b993b4..df61b47ee13 100644 --- a/src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hant.xlf @@ -7,6 +7,11 @@ 根據 NuGet.config Source details text shown for packages from implicit channel or channels without Aspire* package source mappings + + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + The 'staging' channel cannot be resolved deterministically when running a daily/CI Aspire CLI build (CLI version: '{0}'). To update against staging, either run a stable Aspire CLI build, or set the 'overrideStagingFeed' configuration value to an explicit staging feed URL (for example: 'aspire config set overrideStagingFeed <url>'). + Error/warning shown when the 'staging' package channel is requested but the running CLI is itself a daily/CI build with no explicit staging feed override. {0} is the CLI's informational version. + \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 3c562b060e9..24794b0fce8 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -1718,6 +1718,59 @@ private static string GetAspireExecutableName() { return OperatingSystem.IsWindows() ? "aspire.exe" : "aspire"; } + + [Fact] + public async Task UpdateCommand_StagingChannelUnavailable_DisplaysReasonFromPackagingService() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + TestInteractionService? testInteractionService = null; + const string ExpectedReason = "Staging is unavailable on a daily CLI; set 'overrideStagingFeed' to recover."; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.ProjectLocatorFactory = _ => new TestProjectLocator() + { + UseOrFindAppHostProjectFileAsyncCallback = (projectFile, _, _) => + { + return Task.FromResult(new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"))); + } + }; + + options.InteractionServiceFactory = _ => + { + testInteractionService = new TestInteractionService(); + return testInteractionService; + }; + + options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner(); + + options.ProjectUpdaterFactory = _ => new TestProjectUpdater(); + + options.PackagingServiceFactory = _ => new TestPackagingService() + { + GetChannelsAsyncCallback = (ct) => + { + var stableChannel = new PackageChannel("stable", PackageChannelQuality.Stable, null, null!); + var dailyChannel = new PackageChannel("daily", PackageChannelQuality.Prerelease, null, null!); + return Task.FromResult>(new[] { stableChannel, dailyChannel }); + }, + GetStagingChannelUnavailableReasonCallback = () => ExpectedReason, + }; + }); + + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("update --channel staging"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.NotNull(testInteractionService); + var errorMessage = Assert.Single(testInteractionService.DisplayedErrors); + Assert.Contains(ExpectedReason, errorMessage); + Assert.Equal(ExitCodeConstants.FailedToUpgradeProject, exitCode); + } } // Helper class to track DisplayCancellationMessage calls diff --git a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs index 40d8646179f..56231c7cfce 100644 --- a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs @@ -7,6 +7,7 @@ using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System.Xml.Linq; @@ -480,7 +481,11 @@ public async Task GetChannelsAsync_WhenStagingQualityPrerelease_AndNoFeedOverrid var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingQuality"] = "Prerelease" + ["overrideStagingQuality"] = "Prerelease", + // Pin the CLI version for the daily-CLI guard so this test exercises the + // shared-feed code path regardless of the actual test-assembly version + // (which on CI is daily-flavored and would otherwise omit staging). + ["internal:packaging:cliVersionForTesting"] = "13.4.0" }) .Build(); @@ -516,7 +521,10 @@ public async Task GetChannelsAsync_WhenStagingQualityBoth_AndNoFeedOverride_Uses var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingQuality"] = "Both" + ["overrideStagingQuality"] = "Both", + // Pin the CLI version for the daily-CLI guard so this test exercises the + // shared-feed code path regardless of the actual test-assembly version. + ["internal:packaging:cliVersionForTesting"] = "13.4.0" }) .Build(); @@ -588,7 +596,10 @@ public async Task NuGetConfigMerger_WhenStagingUsesSharedFeed_DoesNotAddGlobalPa var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingQuality"] = "Prerelease" + ["overrideStagingQuality"] = "Prerelease", + // Pin the CLI version for the daily-CLI guard so the staging channel is + // created on shared feed regardless of the actual test-assembly version. + ["internal:packaging:cliVersionForTesting"] = "13.4.0" }) .Build(); @@ -634,7 +645,10 @@ public async Task GetChannelsAsync_WhenStagingPinToCliVersionSet_ChannelHasPinne .AddInMemoryCollection(new Dictionary { ["overrideStagingQuality"] = "Prerelease", - ["stagingPinToCliVersion"] = "true" + ["stagingPinToCliVersion"] = "true", + // Pin the CLI version for the daily-CLI guard so this test exercises the + // pinning code path regardless of the actual test-assembly version. + ["internal:packaging:cliVersionForTesting"] = "13.4.0" }) .Build(); @@ -666,8 +680,11 @@ public async Task GetChannelsAsync_WhenStagingPinToCliVersionNotSet_ChannelHasNo var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingQuality"] = "Prerelease" + ["overrideStagingQuality"] = "Prerelease", // No stagingPinToCliVersion + // Pin the CLI version for the daily-CLI guard so this test exercises the + // shared-feed code path regardless of the actual test-assembly version. + ["internal:packaging:cliVersionForTesting"] = "13.4.0" }) .Build(); @@ -803,7 +820,10 @@ public async Task StagingChannel_WithPinnedVersion_ReturnsSyntheticTemplatePacka .AddInMemoryCollection(new Dictionary { ["overrideStagingQuality"] = "Prerelease", - ["stagingPinToCliVersion"] = "true" + ["stagingPinToCliVersion"] = "true", + // Pin the CLI version for the daily-CLI guard so this test exercises the + // shared-feed pinning code path regardless of the actual test-assembly version. + ["internal:packaging:cliVersionForTesting"] = "13.4.0" }) .Build(); @@ -856,7 +876,10 @@ public async Task StagingChannel_WithPinnedVersion_OverridesIntegrationPackageVe .AddInMemoryCollection(new Dictionary { ["overrideStagingQuality"] = "Prerelease", - ["stagingPinToCliVersion"] = "true" + ["stagingPinToCliVersion"] = "true", + // Pin the CLI version for the daily-CLI guard so this test exercises the + // shared-feed pinning code path regardless of the actual test-assembly version. + ["internal:packaging:cliVersionForTesting"] = "13.4.0" }) .Build(); @@ -907,8 +930,11 @@ public async Task StagingChannel_WithoutPinnedVersion_ReturnsAllPrereleasePackag var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingQuality"] = "Prerelease" + ["overrideStagingQuality"] = "Prerelease", // No stagingPinToCliVersion — should return all prerelease + // Pin the CLI version for the daily-CLI guard so this test exercises the + // shared-feed code path regardless of the actual test-assembly version. + ["internal:packaging:cliVersionForTesting"] = "13.4.0" }) .Build(); @@ -933,6 +959,282 @@ public async Task StagingChannel_WithoutPinnedVersion_ReturnsAllPrereleasePackag Assert.Contains(packageList, p => p.Version!.StartsWith("13.2")); } + // ========================================================================================== + // Regression tests for issue #16652: + // Ensure that on a daily/CI Aspire CLI build, `--channel staging` cannot silently resolve + // to daily packages. The `internal:packaging:cliVersionForTesting` configuration key is a + // test-only seam used to deterministically simulate stable / blessed-prerelease / daily + // CLI builds without depending on the actual built CLI version at test time. + // ========================================================================================== + + [Theory] + [InlineData("13.4.0", false)] // stable release → not daily + [InlineData("13.4.0-preview.1", false)] // blessed preview → not daily + [InlineData("13.4.0-rc.1", false)] // blessed RC → not daily + [InlineData("13.4.0-dev", false)] // local dev build → not daily + [InlineData("13.4.0-preview.1.26210.1", true)] // arcade daily build → daily + [InlineData("13.4.0-preview.1.26210.1+abc1234", true)] // daily with build metadata → daily + [InlineData("13.4.0-rc.1.26300.5", true)] // RC-flavored CI build → daily + [InlineData("not-a-valid-version", true)] // unparseable → fail-safe (treat as daily) + [InlineData("", true)] // empty → fail-safe (treat as daily) + [InlineData(null, true)] // null → fail-safe (treat as daily) + public void IsCliPrereleaseDailyBuild_HeuristicProducesExpectedResult(string? informationalVersion, bool expectedIsDaily) + { + var actual = PackagingService.IsCliPrereleaseDailyBuild(informationalVersion); + Assert.Equal(expectedIsDaily, actual); + } + + [Fact] + public async Task GetChannelsAsync_DailyCliRequestsStaging_NoFeedOverride_StagingChannelOmittedAndReasonExplains() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); + + var features = new TestFeatures(); + features.SetFeature(KnownFeatures.StagingChannelEnabled, true); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["internal:packaging:cliVersionForTesting"] = "13.4.0-preview.1.26210.1+abc1234" + }) + .Build(); + + var loggerProvider = new CapturingLoggerProvider(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddProvider(loggerProvider); + }); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, configuration, loggerFactory.CreateLogger()); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + Assert.DoesNotContain(channels, c => c.Name == "staging"); + + var reason = packagingService.GetStagingChannelUnavailableReason(); + Assert.NotNull(reason); + Assert.Contains("staging", reason); + Assert.Contains("daily", reason, StringComparison.OrdinalIgnoreCase); + Assert.Contains("13.4.0-preview.1.26210.1", reason); + Assert.Contains("overrideStagingFeed", reason); + + // Warning was logged so the omission is observable in CLI output. + Assert.Contains(loggerProvider.LogEntries, e => + e.Level == LogLevel.Warning && + e.Message.Contains("staging") && + e.Message.Contains("13.4.0-preview.1.26210.1")); + } + + [Fact] + public async Task GetChannelsAsync_DailyCliRequestsStaging_NoFeedOverride_PrereleaseQuality_AlsoOmitted() + { + // The Prerelease/Both quality + no-feed-override path used to silently fall back + // to the shared dotnet9 daily feed and return daily packages — exactly the bug + // tracked by #16652. On a daily CLI we now block it too. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); + + var features = new TestFeatures(); + features.SetFeature(KnownFeatures.StagingChannelEnabled, true); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["internal:packaging:cliVersionForTesting"] = "13.4.0-preview.1.26210.1", + ["overrideStagingQuality"] = "Prerelease" + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + Assert.DoesNotContain(channels, c => c.Name == "staging"); + Assert.NotNull(packagingService.GetStagingChannelUnavailableReason()); + } + + [Fact] + public async Task GetChannelsAsync_DailyCliRequestsStaging_WithExplicitFeedOverride_UsesOverrideFeed() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); + + var features = new TestFeatures(); + features.SetFeature(KnownFeatures.StagingChannelEnabled, true); + + const string explicitFeedUrl = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-deadbeef/nuget/v3/index.json"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["internal:packaging:cliVersionForTesting"] = "13.4.0-preview.1.26210.1+abc1234", + ["overrideStagingFeed"] = explicitFeedUrl + }) + .Build(); + + var loggerProvider = new CapturingLoggerProvider(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddProvider(loggerProvider); + }); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, configuration, loggerFactory.CreateLogger()); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + // Explicit feed override is honored even on a daily CLI. + var stagingChannel = Assert.Single(channels, c => c.Name == "staging"); + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + + var aspireMapping = stagingChannel.Mappings!.FirstOrDefault(m => m.PackageFilter == "Aspire*"); + Assert.NotNull(aspireMapping); + Assert.Equal(explicitFeedUrl, aspireMapping.Source); + + // Resolved feed URL is logged so users can verify channel resolution from CLI output. + Assert.Contains(loggerProvider.LogEntries, e => + e.Level == LogLevel.Information && + e.Message.Contains(explicitFeedUrl) && + e.Message.Contains("staging")); + } + + [Fact] + public async Task GetChannelsAsync_StableCliRequestsStaging_NoFeedOverride_StagingChannelCreatedFromShaUrl() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); + + var features = new TestFeatures(); + features.SetFeature(KnownFeatures.StagingChannelEnabled, true); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + // A real stable CLI: no prerelease label. + ["internal:packaging:cliVersionForTesting"] = "13.4.0+a1b2c3d4e5f6" + }) + .Build(); + + var loggerProvider = new CapturingLoggerProvider(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddProvider(loggerProvider); + }); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, configuration, loggerFactory.CreateLogger()); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + // Staging channel is created. The exact feed URL on a real stable CLI uses the + // real assembly commit hash; this test only asserts the channel exists and uses + // the darc-pub-* naming pattern so we don't tightly couple to assembly metadata. + var stagingChannel = Assert.Single(channels, c => c.Name == "staging"); + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + + var aspireMapping = stagingChannel.Mappings!.FirstOrDefault(m => m.PackageFilter == "Aspire*"); + Assert.NotNull(aspireMapping); + Assert.StartsWith("https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-", aspireMapping.Source); + + Assert.Contains(loggerProvider.LogEntries, e => + e.Level == LogLevel.Information && + e.Message.Contains("staging") && + e.Message.Contains("darc-pub-microsoft-aspire-")); + } + + [Fact] + public async Task GetChannelsAsync_BlessedPrereleaseCliRequestsStaging_NoFeedOverride_StagingChannelCreated() + { + // Blessed previews/RCs (e.g. "13.4.0-preview.1", "13.4.0-rc.1") have darc-pub-* feeds + // because the build pipeline creates them for those builds. They are NOT daily CI + // builds, so they should still be allowed to synthesize the staging channel. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); + + var features = new TestFeatures(); + features.SetFeature(KnownFeatures.StagingChannelEnabled, true); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["internal:packaging:cliVersionForTesting"] = "13.4.0-rc.1+a1b2c3d4e5f6" + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + Assert.Contains(channels, c => c.Name == "staging"); + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + } + + [Fact] + public void GetStagingChannelUnavailableReason_StagingFeatureDisabled_ReturnsNull() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); + + // Daily CLI but staging is not enabled — there's no "unavailability" to report. + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["internal:packaging:cliVersionForTesting"] = "13.4.0-preview.1.26210.1+abc1234" + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + } + + private sealed class CapturingLoggerProvider : ILoggerProvider + { + public List LogEntries { get; } = new(); + + public ILogger CreateLogger(string categoryName) => new CapturingLogger(categoryName, LogEntries); + + public void Dispose() + { + } + + private sealed class CapturingLogger(string categoryName, List entries) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + lock (entries) + { + entries.Add(new CapturedLogEntry(categoryName, logLevel, formatter(state, exception))); + } + } + } + } + + private sealed record CapturedLogEntry(string Category, LogLevel Level, string Message); + private sealed class FakeNuGetPackageCacheWithPackages(List packages) : INuGetPackageCache { public Task> GetTemplatePackagesAsync(DirectoryInfo workingDirectory, bool prerelease, FileInfo? nugetConfigFile, CancellationToken cancellationToken) diff --git a/tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs b/tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs index 3c58d7b4e45..4677670881e 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs @@ -9,6 +9,8 @@ internal sealed class TestPackagingService : IPackagingService { public Func>>? GetChannelsAsyncCallback { get; set; } + public Func? GetStagingChannelUnavailableReasonCallback { get; set; } + public Task> GetChannelsAsync(CancellationToken cancellationToken = default) { if (GetChannelsAsyncCallback is not null) @@ -20,4 +22,9 @@ public Task> GetChannelsAsync(CancellationToken canc var testChannel = PackageChannel.CreateImplicitChannel(new FakeNuGetPackageCache()); return Task.FromResult>(new[] { testChannel }); } + + public string? GetStagingChannelUnavailableReason() + { + return GetStagingChannelUnavailableReasonCallback?.Invoke(); + } }