diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 3be12832451..ded84d0fd06 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -101,7 +101,7 @@ public NewCommand( // Customize description based on whether staging channel is enabled var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, configuration) - || string.Equals(ExecutionContext.IdentityChannel, PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase); + || string.Equals(ExecutionContext.IdentityChannel, PackageChannelNames.Staging, StringComparisons.ChannelName); _channelOption = new Option("--channel") { Description = isStagingEnabled @@ -333,20 +333,39 @@ private async Task ResolveCliTemplateVersionAsync( !string.IsNullOrWhiteSpace(ExecutionContext.IdentityChannel)) { identityChannelMatch = channels.FirstOrDefault(c => - string.Equals(c.Name, ExecutionContext.IdentityChannel, StringComparison.OrdinalIgnoreCase)); + string.Equals(c.Name, ExecutionContext.IdentityChannel, StringComparisons.ChannelName)); } var selectedChannel = string.IsNullOrWhiteSpace(configuredChannelName) ? identityChannelMatch ?? channels.FirstOrDefault(c => c.Type is PackageChannelType.Implicit) ?? channels.FirstOrDefault() - : channels.FirstOrDefault(c => string.Equals(c.Name, configuredChannelName, StringComparison.OrdinalIgnoreCase)); + : channels.FirstOrDefault(c => string.Equals(c.Name, configuredChannelName, StringComparisons.ChannelName)); if (selectedChannel is null) { - var errorMessage = string.IsNullOrWhiteSpace(configuredChannelName) - ? "No package channels are available." - : $"No channel found matching '{configuredChannelName}'. Valid options are: {string.Join(", ", channels.Select(c => c.Name))}"; + string errorMessage; + if (string.IsNullOrWhiteSpace(configuredChannelName)) + { + errorMessage = NewCommandStrings.NoPackageChannelsAvailable; + } + else if (string.Equals(configuredChannelName, PackageChannelNames.Staging, StringComparisons.ChannelName) + && _packagingService.GetStagingChannelUnavailableReason() is { } stagingReason) + { + // Surface the actionable packaging-service reason (e.g. "daily CLI cannot + // synthesize a staging channel; set overrideStagingFeed") instead of the + // generic channel list, mirroring UpdateCommand's behavior. + // See https://github.com/microsoft/aspire/issues/16652. + errorMessage = stagingReason; + } + else + { + errorMessage = string.Format( + CultureInfo.CurrentCulture, + NewCommandStrings.NoChannelFoundMatching, + configuredChannelName, + string.Join(", ", channels.Select(c => c.Name))); + } return new ResolveTemplateVersionResult { ErrorMessage = errorMessage }; } diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index 172b1f79ca7..2f1fcbc324f 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -208,8 +208,31 @@ protected override async Task ExecuteAsync(ParseResult parseResul if (!string.IsNullOrWhiteSpace(channelName)) { // 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))}"); + var matchedChannel = allChannels.FirstOrDefault(c => string.Equals(c.Name, channelName, StringComparisons.ChannelName)); + if (matchedChannel is null) + { + // When the user explicitly asked for the 'staging' channel and the packaging + // service refused to synthesize it (daily/local/pr-N CLI without an override), + // surface the packaging-service reason instead of the generic "no channel + // matching" message — the generic message hides the actual fix from the user. + // See https://github.com/microsoft/aspire/issues/16652. + if (string.Equals(channelName, PackageChannelNames.Staging, StringComparisons.ChannelName)) + { + var stagingUnavailableReason = _packagingService.GetStagingChannelUnavailableReason(); + if (stagingUnavailableReason is not null) + { + throw new ChannelNotFoundException(stagingUnavailableReason); + } + } + + throw new ChannelNotFoundException(string.Format( + CultureInfo.CurrentCulture, + UpdateCommandStrings.NoChannelFoundMatching, + channelName, + string.Join(", ", allChannels.Select(c => c.Name)))); + } + + channel = matchedChannel; if (channelFromConfig) { @@ -240,9 +263,9 @@ protected override async Task ExecuteAsync(ParseResult parseResul var identityChannel = ExecutionContext.IdentityChannel; PackageChannel? identityMatch = null; if (!string.IsNullOrWhiteSpace(identityChannel) - && !string.Equals(identityChannel, PackageChannelNames.Local, StringComparison.OrdinalIgnoreCase)) + && !string.Equals(identityChannel, PackageChannelNames.Local, StringComparisons.ChannelName)) { - identityMatch = allChannels.FirstOrDefault(c => string.Equals(c.Name, identityChannel, StringComparison.OrdinalIgnoreCase)); + identityMatch = allChannels.FirstOrDefault(c => string.Equals(c.Name, identityChannel, StringComparisons.ChannelName)); } if (identityMatch is not null) @@ -362,7 +385,7 @@ protected override async Task ExecuteAsync(ParseResult parseResul private bool IsStagingChannelAvailable() { return KnownFeatures.IsStagingChannelEnabled(_features, _configuration) - || string.Equals(ExecutionContext.IdentityChannel, PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase); + || string.Equals(ExecutionContext.IdentityChannel, PackageChannelNames.Staging, StringComparisons.ChannelName); } private async Task ExecuteSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken, string? selectedChannel = null) diff --git a/src/Aspire.Cli/KnownFeatures.cs b/src/Aspire.Cli/KnownFeatures.cs index 030a6298441..dc0f3202b23 100644 --- a/src/Aspire.Cli/KnownFeatures.cs +++ b/src/Aspire.Cli/KnownFeatures.cs @@ -127,6 +127,6 @@ public static IEnumerable GetAllFeatureNames() public static bool IsStagingChannelEnabled(IFeatures features, IConfiguration configuration) { return features.IsFeatureEnabled(StagingChannelEnabled, false) - || string.Equals(configuration["channel"], PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase); + || string.Equals(configuration["channel"], PackageChannelNames.Staging, StringComparisons.ChannelName); } } diff --git a/src/Aspire.Cli/Packaging/PackagingService.cs b/src/Aspire.Cli/Packaging/PackagingService.cs index 822fd216a35..93b2b633ce4 100644 --- a/src/Aspire.Cli/Packaging/PackagingService.cs +++ b/src/Aspire.Cli/Packaging/PackagingService.cs @@ -3,10 +3,12 @@ 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.Globalization; using System.Reflection; namespace Aspire.Cli.Packaging; @@ -14,24 +16,77 @@ namespace Aspire.Cli.Packaging; internal interface IPackagingService { public Task> GetChannelsAsync(CancellationToken cancellationToken = default, string? requestedChannelName = null); + + /// + /// Returns a user-facing reason explaining why the staging package channel cannot be + /// synthesized for the running CLI, or when staging IS available. + /// + /// + /// On a CLI whose baked AspireCliChannel identity is daily, local, or + /// pr-<N>, there is no deterministic way to produce a real staging feed: + /// the SHA-specific darc feed (darc-pub-microsoft-aspire-<hash>) only exists + /// for stable release branch builds, and falling back to the shared daily feed silently + /// resolves daily packages instead of staging ones. To avoid that downgrade + /// (see ), the service refuses + /// to fabricate a staging channel from those identities unless the caller has set + /// overrideStagingFeed or enabled the staging feature flag. + /// + string? GetStagingChannelUnavailableReason(); } -internal class PackagingService(CliExecutionContext executionContext, INuGetPackageCache nuGetPackageCache, IFeatures features, IConfiguration configuration, ILogger logger) : IPackagingService +internal class PackagingService : IPackagingService { + // Configuration key used to override the staging feed URL. When non-empty, + // PackagingService treats staging as available regardless of the CLI's + // identity channel (see IsStagingChannelSynthesisAllowed). Surfaced from + // tests via InternalsVisibleTo so a single literal change can't drift. + internal const string OverrideStagingFeedConfigKey = "overrideStagingFeed"; + + private readonly CliExecutionContext _executionContext; + private readonly INuGetPackageCache _nuGetPackageCache; + private readonly IFeatures _features; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + // Cached result of the staging-channel availability check. The inputs (CLI identity, + // overrideStagingFeed, StagingChannelEnabled feature) are effectively static for the + // process lifetime, so computing this once avoids re-formatting the localized reason + // string on every GetChannelsAsync call (callers fan out across NewCommand, + // UpdateCommand, IntegrationPackageSearchService, NuGetPackagePrefetcher, etc.). + private readonly Lazy _stagingUnavailableReasonCache; + + public PackagingService(CliExecutionContext executionContext, INuGetPackageCache nuGetPackageCache, IFeatures features, IConfiguration configuration, ILogger logger) + { + _executionContext = executionContext; + _nuGetPackageCache = nuGetPackageCache; + _features = features; + _configuration = configuration; + _logger = logger; + _stagingUnavailableReasonCache = new Lazy(ComputeStagingChannelUnavailableReason); + } + + // One-shot guards so the refusal warning / successful-resolution info line are emitted + // at most once per CLI process instead of on every GetChannelsAsync invocation. Many + // commands (and the background NuGetPackagePrefetcher) call GetChannelsAsync repeatedly; + // logging on each call produced excessive noise — particularly the refusal warning when + // a project's aspire.config.json pins `channel: staging` on a daily/local CLI. + private int _stagingRefusalLogged; + private int _stagingResolutionLogged; + public Task> GetChannelsAsync(CancellationToken cancellationToken = default, string? requestedChannelName = null) { - var defaultChannel = PackageChannel.CreateImplicitChannel(nuGetPackageCache, logger); + var defaultChannel = PackageChannel.CreateImplicitChannel(_nuGetPackageCache, _logger); var stableChannel = PackageChannel.CreateExplicitChannel(PackageChannelNames.Stable, PackageChannelQuality.Stable, new[] { new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json") - }, nuGetPackageCache, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/ga/daily", logger: logger); + }, _nuGetPackageCache, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/ga/daily", logger: _logger); var dailyChannel = PackageChannel.CreateExplicitChannel(PackageChannelNames.Daily, PackageChannelQuality.Prerelease, new[] { new PackageMapping("Aspire*", "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json"), new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json") - }, nuGetPackageCache, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/daily", logger: logger); + }, _nuGetPackageCache, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/daily", logger: _logger); var prPackageChannels = new List(); @@ -39,9 +94,9 @@ public Task> GetChannelsAsync(CancellationToken canc // intermediate directory structure which may not exist in some // contexts (e.g. in our Codespace where we have the CLI on the // path but not in the $HOME/.aspire/bin folder). - if (executionContext.HivesDirectory.Exists) + if (_executionContext.HivesDirectory.Exists) { - var prHives = executionContext.HivesDirectory.GetDirectories(); + var prHives = _executionContext.HivesDirectory.GetDirectories(); foreach (var prHive in prHives) { // The packages subdirectory contains the actual .nupkg files @@ -54,7 +109,7 @@ public Task> GetChannelsAsync(CancellationToken canc { new PackageMapping("Aspire*", packagesPath), new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json") - }, nuGetPackageCache, pinnedVersion: pinnedVersion, logger: logger); + }, _nuGetPackageCache, pinnedVersion: pinnedVersion, logger: _logger); prPackageChannels.Add(prChannel); } @@ -66,10 +121,10 @@ public Task> GetChannelsAsync(CancellationToken canc // dogfood staging packages even before a project-level channel pin exists, and // callers that already resolved a staging channel from another project directory // need the channel materialized before they can match it below. - var stagingChannelConfigured = string.Equals(configuration["channel"], PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase); - var stagingChannelRequested = string.Equals(requestedChannelName, PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase); - var stagingIdentityChannel = string.Equals(executionContext.IdentityChannel, PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase); - var stagingFeatureEnabled = features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false); + var stagingChannelConfigured = string.Equals(_configuration["channel"], PackageChannelNames.Staging, StringComparisons.ChannelName); + var stagingChannelRequested = string.Equals(requestedChannelName, PackageChannelNames.Staging, StringComparisons.ChannelName); + var stagingIdentityChannel = string.Equals(_executionContext.IdentityChannel, PackageChannelNames.Staging, StringComparisons.ChannelName); + var stagingFeatureEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false); if (stagingFeatureEnabled || stagingChannelConfigured || stagingChannelRequested || stagingIdentityChannel) { var defaultQuality = stagingChannelConfigured || stagingChannelRequested || stagingIdentityChannel ? PackageChannelQuality.Both : PackageChannelQuality.Stable; @@ -89,8 +144,24 @@ public Task> GetChannelsAsync(CancellationToken canc private PackageChannel? CreateStagingChannel(PackageChannelQuality defaultQuality) { + // Refuse to synthesize a staging channel on CLI identities that cannot produce a real + // staging feed (daily, local, pr-). Silently falling back to the shared daily feed or + // a non-existent SHA-specific darc feed is the bug tracked by + // https://github.com/microsoft/aspire/issues/16652. The escape hatches (explicit + // overrideStagingFeed, or the StagingChannelEnabled feature flag) are honored inside + // IsStagingChannelSynthesisAllowed below. + var unavailableReason = GetStagingChannelUnavailableReason(); + if (unavailableReason is not null) + { + if (Interlocked.Exchange(ref _stagingRefusalLogged, 1) == 0) + { + _logger.LogWarning("Refusing to synthesize 'staging' package channel: {Reason}", unavailableReason); + } + return null; + } + var stagingQuality = GetStagingQuality(defaultQuality); - var hasExplicitFeedOverride = !string.IsNullOrEmpty(configuration["overrideStagingFeed"]); + var hasExplicitFeedOverride = !string.IsNullOrEmpty(_configuration[OverrideStagingFeedConfigKey]); // When quality is Prerelease or Both and no explicit feed override is set, // use the shared daily feed instead of the SHA-specific feed. SHA-specific @@ -111,15 +182,72 @@ public Task> GetChannelsAsync(CancellationToken canc { new PackageMapping("Aspire*", stagingFeedUrl), 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); + }, _nuGetPackageCache, configureGlobalPackagesFolder: !useSharedFeed, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/rc/daily", pinnedVersion: pinnedVersion, logger: _logger); + + // Surface the resolved staging routing so users can see what `--channel staging` actually + // picked (the "show what was resolved" suggestion from the issue RCA). Pinned version is + // optional and only set when configured via stagingPinToCliVersion. Emitted once per + // process to avoid repeating on every GetChannelsAsync call. + if (Interlocked.Exchange(ref _stagingResolutionLogged, 1) == 0) + { + _logger.LogInformation( + "Resolved 'staging' channel: feed={FeedUrl}, quality={Quality}, pinnedVersion={PinnedVersion}", + stagingFeedUrl, + stagingQuality, + pinnedVersion ?? "(none)"); + } return stagingChannel; } + /// + public string? GetStagingChannelUnavailableReason() => _stagingUnavailableReasonCache.Value; + + private string? ComputeStagingChannelUnavailableReason() + { + if (IsStagingChannelSynthesisAllowed()) + { + return null; + } + + return string.Format( + CultureInfo.CurrentCulture, + PackagingStrings.StagingChannelUnavailableOnDailyCli, + _executionContext.IdentityChannel); + } + + private bool IsStagingChannelSynthesisAllowed() + { + // Explicit feed override always wins: the caller has told us exactly which feed to use, + // so we don't need to infer one from the CLI identity. + if (!string.IsNullOrEmpty(_configuration[OverrideStagingFeedConfigKey])) + { + return true; + } + + // The staging feature flag is an explicit developer/test opt-in that predates this + // gating; preserve it for back-compat with existing developer workflows. + if (_features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false)) + { + return true; + } + + // Only stable and staging CLI builds can deterministically resolve a staging feed: + // - stable: the SHA-specific darc-pub-microsoft-aspire- feed exists for the + // stable release branch commit baked into the CLI. + // - staging: dogfoods staging packages (see #17155 which auto-registers the staging + // channel for the staging CLI identity). + // For daily, local, and pr- identities, falling back to either the SHA feed (no real + // darc feed exists) or the shared daily feed silently resolves daily packages — the + // exact bug tracked by https://github.com/microsoft/aspire/issues/16652. + return string.Equals(_executionContext.IdentityChannel, PackageChannelNames.Stable, StringComparisons.ChannelName) + || string.Equals(_executionContext.IdentityChannel, PackageChannelNames.Staging, StringComparisons.ChannelName); + } + private string? GetStagingFeedUrl(bool useSharedFeed) { - // Check for configuration override first - var overrideFeed = configuration["overrideStagingFeed"]; + // Check for _configuration override first + var overrideFeed = _configuration[OverrideStagingFeedConfigKey]; if (!string.IsNullOrEmpty(overrideFeed)) { // Validate that the override URL is well-formed @@ -163,8 +291,8 @@ public Task> GetChannelsAsync(CancellationToken canc private PackageChannelQuality GetStagingQuality(PackageChannelQuality defaultQuality) { - // Check for configuration override - var overrideQuality = configuration["overrideStagingQuality"]; + // Check for _configuration override + var overrideQuality = _configuration["overrideStagingQuality"]; if (!string.IsNullOrEmpty(overrideQuality)) { // Try to parse the quality value (case-insensitive) @@ -182,7 +310,7 @@ private PackageChannelQuality GetStagingQuality(PackageChannelQuality defaultQua private string? GetStagingPinnedVersion(bool useSharedFeed) { // Only pin versions when using the shared feed and the config flag is set - var pinToCliVersion = configuration["stagingPinToCliVersion"]; + var pinToCliVersion = _configuration["stagingPinToCliVersion"]; if (!useSharedFeed || !string.Equals(pinToCliVersion, "true", StringComparison.OrdinalIgnoreCase)) { return null; diff --git a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs index a00a7aa8605..79e44fc9fd5 100644 --- a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs +++ b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs @@ -458,11 +458,41 @@ internal static string GenerateIntegrationProjectFile( return channelName; } + /// + /// Throws when the caller asked for the staging channel but the running CLI's packaging + /// service refuses to synthesize one (daily/local/pr-N identity without + /// overrideStagingFeed or the StagingChannelEnabled feature flag). Surfaces + /// the same actionable reason the update and new commands display so the + /// bundled AppHost restore path doesn't silently downgrade to the daily feed. + /// + private void ThrowIfStagingUnavailable(string? requestedChannel) + { + if (!string.Equals(requestedChannel, PackageChannelNames.Staging, StringComparisons.ChannelName)) + { + return; + } + + var reason = _packagingService.GetStagingChannelUnavailableReason(); + if (reason is not null) + { + throw new InvalidOperationException(reason); + } + } + /// /// Gets NuGet sources from the resolved channel for bundled restore. /// - private async Task?> GetNuGetSourcesAsync(string? requestedChannel, CancellationToken cancellationToken) + internal async Task?> GetNuGetSourcesAsync(string? requestedChannel, CancellationToken cancellationToken) { + // Refuse to silently downgrade staging restores to the shared daily feed when the running + // CLI cannot synthesize a real staging channel (daily/local/pr-). PackagingService omits + // the staging channel in that case; without this check the lookup below falls through to + // "all explicit channels" — which on a daily CLI is the shared daily feed — and restore + // silently succeeds against the wrong feed. Surfacing the actionable + // GetStagingChannelUnavailableReason() mirrors UpdateCommand/NewCommand and closes the + // bundled-AppHost arm of https://github.com/microsoft/aspire/issues/16652. + ThrowIfStagingUnavailable(requestedChannel); + var sources = new List(); try @@ -472,7 +502,7 @@ internal static string GenerateIntegrationProjectFile( IEnumerable explicitChannels; if (!string.IsNullOrEmpty(requestedChannel)) { - var matchingChannel = channels.FirstOrDefault(c => string.Equals(c.Name, requestedChannel, StringComparison.OrdinalIgnoreCase)); + var matchingChannel = channels.FirstOrDefault(c => string.Equals(c.Name, requestedChannel, StringComparisons.ChannelName)); explicitChannels = matchingChannel is not null ? [matchingChannel] : channels.Where(c => c.Type == PackageChannelType.Explicit); } else @@ -504,18 +534,23 @@ internal static string GenerateIntegrationProjectFile( return sources.Count > 0 ? sources : null; } - private async Task TryCreateTemporaryNuGetConfigAsync(string? requestedChannel, CancellationToken cancellationToken) + internal async Task TryCreateTemporaryNuGetConfigAsync(string? requestedChannel, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(requestedChannel)) { return null; } + // Same staging refusal as GetNuGetSourcesAsync: if the CLI cannot synthesize staging, + // surface the actionable reason instead of returning null and letting restore proceed + // against whichever sources the caller resolved separately. + ThrowIfStagingUnavailable(requestedChannel); + var channels = await _packagingService.GetChannelsAsync(cancellationToken, requestedChannel); var channel = channels.FirstOrDefault(c => c.Type == PackageChannelType.Explicit && c.Mappings is { Length: > 0 } && - string.Equals(c.Name, requestedChannel, StringComparison.OrdinalIgnoreCase)); + string.Equals(c.Name, requestedChannel, StringComparisons.ChannelName)); if (channel?.Mappings is null) { @@ -529,7 +564,7 @@ internal static string GenerateIntegrationProjectFile( // regardless of which CLI identity (CliExecutionContext.IdentityChannel) is running. // Keying on the resolved channel.Name (rather than the input requestedChannel) is robust // to alias/normalization in the channel lookup above. - if (string.Equals(channel.Name, PackageChannelNames.Local, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(channel.Name, PackageChannelNames.Local, StringComparisons.ChannelName)) { return null; } diff --git a/src/Aspire.Cli/Resources/NewCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/NewCommandStrings.Designer.cs index bf178cbb740..001689d8bf3 100644 --- a/src/Aspire.Cli/Resources/NewCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/NewCommandStrings.Designer.cs @@ -168,5 +168,17 @@ public static string OutputPathContainsInvalidCharacters { return ResourceManager.GetString("OutputPathContainsInvalidCharacters", resourceCulture); } } + + public static string NoPackageChannelsAvailable { + get { + return ResourceManager.GetString("NoPackageChannelsAvailable", resourceCulture); + } + } + + public static string NoChannelFoundMatching { + get { + return ResourceManager.GetString("NoChannelFoundMatching", resourceCulture); + } + } } } diff --git a/src/Aspire.Cli/Resources/NewCommandStrings.resx b/src/Aspire.Cli/Resources/NewCommandStrings.resx index 2ac072bbe8e..32b4b84bc45 100644 --- a/src/Aspire.Cli/Resources/NewCommandStrings.resx +++ b/src/Aspire.Cli/Resources/NewCommandStrings.resx @@ -163,4 +163,10 @@ The output path '{0}' contains invalid characters. + + No package channels are available. + + + No channel found matching '{0}'. Valid options are: {1}. + diff --git a/src/Aspire.Cli/Resources/PackagingStrings.Designer.cs b/src/Aspire.Cli/Resources/PackagingStrings.Designer.cs index 0d28888627e..f578e897705 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + /// + internal static string StagingChannelUnavailableOnDailyCli { + get { + return ResourceManager.GetString("StagingChannelUnavailableOnDailyCli", resourceCulture); + } + } } } diff --git a/src/Aspire.Cli/Resources/PackagingStrings.resx b/src/Aspire.Cli/Resources/PackagingStrings.resx index bd670970a4b..968126087f1 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs index 0d4c484e9f6..93207cc6173 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs @@ -121,5 +121,6 @@ internal static string ProjectArgumentDescription { internal static string SelfOptionDescription => ResourceManager.GetString("SelfOptionDescription", resourceCulture); internal static string YesOptionDescription => ResourceManager.GetString("YesOptionDescription", resourceCulture); internal static string NuGetConfigDirOptionDescription => ResourceManager.GetString("NuGetConfigDirOptionDescription", resourceCulture); + internal static string NoChannelFoundMatching => ResourceManager.GetString("NoChannelFoundMatching", resourceCulture); } } diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx index da2554688c5..2329e5a85a4 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx @@ -180,4 +180,7 @@ Directory to create or update the NuGet.config file in + + No channel found matching '{0}'. Valid options are: {1}. + diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.cs.xlf index f93d8829bc3..75421626a0e 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.cs.xlf @@ -42,6 +42,16 @@ Název projektu, který se má vytvořit + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.de.xlf index e80987a2972..e1aa7b2e31f 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.de.xlf @@ -42,6 +42,16 @@ Der Name des zu erstellenden Projekts. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.es.xlf index b75161b2255..7c437f8c5fc 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.es.xlf @@ -42,6 +42,16 @@ El nombre del proyecto que se va a crear. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.fr.xlf index 00f9bab9a14..3d1b9b0afd3 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.fr.xlf @@ -42,6 +42,16 @@ Nom du projet à créer. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.it.xlf index ff4fe554a62..b37631f57f2 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.it.xlf @@ -42,6 +42,16 @@ Nome del progetto da creare. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ja.xlf index 053b9f32596..9558a64f195 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ja.xlf @@ -42,6 +42,16 @@ 作成するプロジェクトの名前。 + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ko.xlf index bd5397e71ec..c3ce8d97d5b 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ko.xlf @@ -42,6 +42,16 @@ 만들 프로젝트의 이름입니다. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.pl.xlf index bb465842f94..0bdc487fd55 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.pl.xlf @@ -42,6 +42,16 @@ Nazwa projektu do utworzenia. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.pt-BR.xlf index cd28e077743..e0bff8a3107 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.pt-BR.xlf @@ -42,6 +42,16 @@ O nome do projeto a ser criado. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ru.xlf index a5f0577a46b..cbfc42fb7ee 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.ru.xlf @@ -42,6 +42,16 @@ Имя создаваемого проекта. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.tr.xlf index 3076d807dbf..741f73b1a5c 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.tr.xlf @@ -42,6 +42,16 @@ Oluşturulacak projenin adı. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.zh-Hans.xlf index b46875ab027..d2b4bd90e7f 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.zh-Hans.xlf @@ -42,6 +42,16 @@ 要创建的项目的名称。 + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.zh-Hant.xlf index 0328db084d1..5539a753ad3 100644 --- a/src/Aspire.Cli/Resources/xlf/NewCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/NewCommandStrings.zh-Hant.xlf @@ -42,6 +42,16 @@ 要建立專案之名稱。 + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + + + No package channels are available. + No package channels are available. + + A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. A template must be specified when running in non-interactive mode. Use 'aspire new <template>'. diff --git a/src/Aspire.Cli/Resources/xlf/PackagingStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/PackagingStrings.cs.xlf index 47bc6a1a595..f5468a30ea7 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..b4145bb75a1 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..de1474327e2 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..05f7df7f824 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..10d617cd8c7 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..b376d875f4c 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..7caf35facee 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..5fd89b44f6f 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..16bfa5e04c8 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..86302f3cff8 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..d5ea74d62c2 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..3bcedb4c54e 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ 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..4100f19c314 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 from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + The 'staging' channel cannot be resolved from a CLI with identity '{0}'. Daily, local, and per-PR CLI builds do not have a deterministic staging feed. Install a staging or stable Aspire CLI, or set 'overrideStagingFeed' (for example: aspire config set -g overrideStagingFeed <feed-url>) to point at the staging feed you want to use. + {0} is the running CLI's baked identity channel (one of daily, local, or pr-N). Shown when --channel staging is requested on a CLI that cannot produce a real staging feed. Tracks https://github.com/microsoft/aspire/issues/16652. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf index 37af2a20e74..8a67bad785b 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf @@ -152,6 +152,11 @@ V souboru NuGet.config se nezjistily žádné změny. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. V kanálu {1} nebyl nalezen žádný balíček s ID {0}. @@ -284,4 +289,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf index 24bc7f4123f..061cd606b25 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf @@ -152,6 +152,11 @@ In NuGet.config wurden keine Änderungen festgestellt. + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. Im Kanal „{0}“ wurde kein Paket mit der ID „{1}“ gefunden. diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf index 85c08a437b5..4d0956fdb7a 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf @@ -152,6 +152,11 @@ No se detectaron cambios en NuGet.config + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. No se encontró ningún paquete con id. "{0}" en el canal "{1}". diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf index 4c8c2516e09..91267ce4020 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf @@ -152,6 +152,11 @@ Aucune modification n’a été détectée dans NuGet.config + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. Aucun package trouvé avec l’ID « {0} » dans le canal « {1} ». diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf index 925cdb9d705..adff171d6fd 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf @@ -152,6 +152,11 @@ Nessuna modifica rilevata in NuGet.config + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. Non sono stati trovati pacchetti con ID "{0}" nel canale "{1}". diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf index a258c70649b..f962c19dad5 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf @@ -152,6 +152,11 @@ NuGet.config に変更は検出されませんでした + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. チャネル '{1}' 内に、ID '{0}' を含むパッケージが見つかりませんでした。 diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf index 968e8401be9..b151ff2c3d0 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf @@ -152,6 +152,11 @@ NuGet.config에서 변경 내용이 검색되지 않음 + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. 채널 '{1}'에서 ID '{0}'의 패키지를 찾을 수 없습니다. diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf index 314ae6cdf9c..ce41cc99244 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf @@ -152,6 +152,11 @@ Nie wykryto żadnych zmian w pliku NuGet.config + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. Nie znaleziono pakietu o identyfikatorze „{0}” w kanale „{1}”. diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf index c85fe277916..0f2fb277160 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf @@ -152,6 +152,11 @@ Nenhuma alteração detectada no NuGet.config + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. Nenhum pacote encontrado com a ID “{0}” no canal “{1}”. diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf index 45de85bc737..1084ea024b9 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf @@ -152,6 +152,11 @@ Изменений в NuGet.config не обнаружено + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. Не найден пакет с идентификатором "{0}" в канале "{1}". diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf index 2167496e009..c3a644be0e5 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf @@ -152,6 +152,11 @@ NuGet.config dosyasında değişiklik algılanmadı + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. '{1}' kanalında '{0}' kimliğine sahip paket bulunamadı. diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf index d4a9c6432f7..8ebb4e826df 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf @@ -152,6 +152,11 @@ NuGet.config 中未检测到任何更改 + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. 在频道 "{0}" 中找不到 ID 为 "{1}" 的包。 diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf index 0f3dd5bcc51..cdee5937beb 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf @@ -152,6 +152,11 @@ 未偵測到 NuGet.config 中的變更 + + No channel found matching '{0}'. Valid options are: {1}. + No channel found matching '{0}'. Valid options are: {1}. + + No package found with ID '{0}' in channel '{1}'. 在通道 '{1}' 中找不到識別碼為 '{0}' 的套件。 diff --git a/src/Shared/StringComparers.cs b/src/Shared/StringComparers.cs index 8c1753348f0..41ef4ed1f9a 100644 --- a/src/Shared/StringComparers.cs +++ b/src/Shared/StringComparers.cs @@ -35,6 +35,7 @@ internal static class StringComparers public static StringComparer NetworkID => StringComparer.Ordinal; public static StringComparer NuGetPackageId => StringComparer.OrdinalIgnoreCase; public static StringComparer FullTextSearch => StringComparer.OrdinalIgnoreCase; + public static StringComparer ChannelName => StringComparer.OrdinalIgnoreCase; } internal static class StringComparisons @@ -67,4 +68,5 @@ internal static class StringComparisons public static StringComparison NetworkID => StringComparison.Ordinal; public static StringComparison NuGetPackageId => StringComparison.OrdinalIgnoreCase; public static StringComparison FullTextSearch => StringComparison.OrdinalIgnoreCase; + public static StringComparison ChannelName => StringComparison.OrdinalIgnoreCase; } diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 457bcd480ca..ba03f8a10b6 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -1350,6 +1350,71 @@ public async Task UpdateCommand_ConfiguredChannelNotInChannelList_ThrowsChannelN Assert.Equal(CliExitCodes.FailedToUpgradeProject, exitCode); } + [Fact] + public async Task UpdateCommand_ChannelStagingRequestedButPackagingServiceReportsUnavailable_SurfacesStagingReason() + { + // Regression: https://github.com/microsoft/aspire/issues/16652 + // When `aspire update --channel staging` is run on a daily/local/pr-N CLI, the packaging + // service refuses to synthesize the staging channel and returns a reason explaining why. + // UpdateCommand must surface that reason in its error path instead of the generic + // "No channel found matching 'staging'" message — the generic message hides the actual + // recovery action (set overrideStagingFeed or install a staging CLI) from the user. + using var workspace = TemporaryWorkspace.Create(outputHelper); + + const string unavailableReason = "FAKE staging unavailable reason (identity=daily)"; + + TestInteractionService? capturedInteractionService = null; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.ProjectLocatorFactory = _ => new TestProjectLocator + { + UseOrFindAppHostProjectFileAsyncCallback = (projectFile, _, _) => + Task.FromResult(new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"))) + }; + + options.InteractionServiceFactory = _ => + { + capturedInteractionService = new TestInteractionService(); + return capturedInteractionService; + }; + + options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner(); + options.ProjectUpdaterFactory = _ => new TestProjectUpdater(); + + options.PackagingServiceFactory = _ => new TestPackagingService + { + // Simulate the production PackagingService refusing staging synthesis: staging is + // omitted from the channel list AND GetStagingChannelUnavailableReason() reports + // a non-null, user-facing reason. + GetChannelsAsyncCallback = _ => + { + var fakeCache = new FakeNuGetPackageCache(); + return Task.FromResult>(new[] + { + PackageChannel.CreateImplicitChannel(fakeCache), + PackageChannel.CreateExplicitChannel("daily", PackageChannelQuality.Both, mappings: null, fakeCache), + }); + }, + GetStagingChannelUnavailableReasonCallback = () => unavailableReason + }; + }); + + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("update --channel staging"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.FailedToUpgradeProject, exitCode); + + Assert.NotNull(capturedInteractionService); + var errorText = string.Join("\n", capturedInteractionService!.DisplayedErrors); + Assert.Contains(unavailableReason, errorText); + Assert.DoesNotContain("No channel found matching", errorText); + } + [Fact] public async Task UpdateCommand_ProjectInOtherDirectory_UsesProjectLocalConfiguredChannel() { diff --git a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs index dab0dcaeabf..86ea16ff0c2 100644 --- a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs @@ -62,7 +62,7 @@ public async Task GetChannelsAsync_WhenIdentityChannelIsStaging_IncludesStagingC var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = "https://example.com/nuget/v3/index.json" + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json" }) .Build(); var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); @@ -100,8 +100,13 @@ public async Task GetChannelsAsync_WhenRequestedChannelIsStaging_IncludesStaging } [Fact] - public async Task GetChannelsAsync_WhenConfigurationChannelIsStagingWithoutQualityOverride_DefaultsToBothAndSharedFeed() + public async Task GetChannelsAsync_WhenConfigurationChannelIsStagingOnLocalCli_DoesNotIncludeStagingChannel() { + // Regression: https://github.com/microsoft/aspire/issues/16652 + // A local/daily/pr-N CLI must not silently fabricate a 'staging' channel from the shared + // daily feed when config asks for staging — that resolves daily packages, not staging. + // The escape hatches (overrideStagingFeed or the staging feature flag) are covered by + // other tests in this file. using var workspace = TemporaryWorkspace.Create(outputHelper); var tempDir = workspace.WorkspaceRoot; var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); @@ -118,12 +123,163 @@ public async Task GetChannelsAsync_WhenConfigurationChannelIsStagingWithoutQuali var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + var channelNames = channels.Select(c => c.Name).ToList(); + Assert.DoesNotContain(PackageChannelNames.Staging, channelNames); + + var reason = packagingService.GetStagingChannelUnavailableReason(); + Assert.NotNull(reason); + Assert.Contains(PackageChannelNames.Local, reason); + } + + [Fact] + public async Task GetChannelsAsync_WhenConfigurationChannelIsStagingOnStableCli_IncludesStagingChannelWithSharedFeed() + { + // Counterpart to the local/daily refusal: a stable-identity CLI can synthesize staging + // because the SHA-specific darc feed for the stable commit exists. With quality=Both + // (the default for stagingChannelConfigured), useSharedFeed=true so the channel points + // at the shared dnceng/dotnet9 feed. + 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", identityChannel: PackageChannelNames.Stable); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["channel"] = PackageChannelNames.Staging + }) + .Build(); + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + var stagingChannel = channels.First(c => c.Name == PackageChannelNames.Staging); Assert.Equal(PackageChannelQuality.Both, stagingChannel.Quality); Assert.False(stagingChannel.ConfigureGlobalPackagesFolder); Assert.Contains(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*" && m.Source == "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json"); + + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + } + + [Fact] + public async Task GetChannelsAsync_WhenChannelStagingRequestedOnDailyCli_DoesNotIncludeStagingChannel() + { + // Direct repro of https://github.com/microsoft/aspire/issues/16652. + // A daily CLI invoked with `aspire update --channel staging` must NOT synthesize a + // staging channel from either the SHA-specific darc feed (which doesn't exist for daily + // commits) or the shared daily feed (which contains daily packages, not staging). + 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", identityChannel: PackageChannelNames.Daily); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), new ConfigurationBuilder().Build(), NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync(requestedChannelName: PackageChannelNames.Staging).DefaultTimeout(); + + var channelNames = channels.Select(c => c.Name).ToList(); + Assert.DoesNotContain(PackageChannelNames.Staging, channelNames); + + var reason = packagingService.GetStagingChannelUnavailableReason(); + Assert.NotNull(reason); + Assert.Contains(PackageChannelNames.Daily, reason); + } + + [Fact] + public async Task GetChannelsAsync_WhenChannelStagingRequestedOnDailyCliWithOverrideFeed_IncludesStagingChannel() + { + // The overrideStagingFeed escape hatch must still work on a daily CLI: when the user has + // explicitly named the staging feed, we trust them and synthesize the 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", identityChannel: PackageChannelNames.Daily); + + var overrideUrl = "https://example.com/staging/v3/index.json"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideStagingFeedConfigKey] = overrideUrl + }) + .Build(); + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync(requestedChannelName: PackageChannelNames.Staging).DefaultTimeout(); + + var stagingChannel = channels.First(c => c.Name == PackageChannelNames.Staging); + Assert.Contains(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*" && m.Source == overrideUrl); + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + } + + [Theory] + [InlineData(PackageChannelNames.Local)] + [InlineData("pr-12345")] + public async Task GetChannelsAsync_WhenChannelStagingRequestedOnNonReleaseIdentityWithoutOverride_DoesNotIncludeStagingChannel(string identity) + { + // The same gating applies to local and per-PR CLI identities. Per-PR (pr-) builds + // have a hive label baked in by CI but no staging feed of their own. + 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", identityChannel: identity); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), new ConfigurationBuilder().Build(), NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync(requestedChannelName: PackageChannelNames.Staging).DefaultTimeout(); + + Assert.DoesNotContain(PackageChannelNames.Staging, channels.Select(c => c.Name)); + + var reason = packagingService.GetStagingChannelUnavailableReason(); + Assert.NotNull(reason); + Assert.Contains(identity, reason); + } + + [Fact] + public async Task GetChannelsAsync_WhenChannelStagingRequestedOnDailyCliWithFeatureFlag_IncludesStagingChannel() + { + // Back-compat: the StagingChannelEnabled feature flag is an explicit developer/test opt-in + // and continues to bypass the identity gating. Without an override feed the SHA-specific + // path needs an AssemblyInformationalVersion to resolve, which is not guaranteed in test + // hosts, so we also supply overrideStagingFeed to make the test deterministic. + 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", identityChannel: PackageChannelNames.Daily); + + var features = new TestFeatures(); + features.SetFeature(KnownFeatures.StagingChannelEnabled, true); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/staging/v3/index.json" + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + Assert.Contains(PackageChannelNames.Staging, channels.Select(c => c.Name)); + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + + // Isolate the feature-flag gate itself: IsStagingChannelSynthesisAllowed short-circuits on + // overrideStagingFeed before the feature flag is ever checked, so the assertions above + // would still pass if the feature-flag branch were removed. Build a second service whose + // only opt-in is the StagingChannelEnabled feature flag (no overrideStagingFeed) and + // assert that the gate alone reports the channel as available. We deliberately do not + // call GetChannelsAsync() here because the full channel-creation path requires an + // AssemblyInformationalVersion that is not guaranteed in test hosts. + var featureFlagOnlyConfig = new ConfigurationBuilder().Build(); + var featureFlagOnlyService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, featureFlagOnlyConfig, NullLogger.Instance); + Assert.Null(featureFlagOnlyService.GetStagingChannelUnavailableReason()); } /// @@ -176,7 +332,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabled_IncludesStagingChan var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = testFeedUrl + [PackagingService.OverrideStagingFeedConfigKey] = testFeedUrl }) .Build(); @@ -219,7 +375,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithOverrideFeed_Use var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = customFeedUrl + [PackagingService.OverrideStagingFeedConfigKey] = customFeedUrl }) .Build(); @@ -251,7 +407,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithAzureDevOpsFeedO var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = azureDevOpsFeedUrl + [PackagingService.OverrideStagingFeedConfigKey] = azureDevOpsFeedUrl }) .Build(); @@ -283,7 +439,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithInvalidOverrideF var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = invalidFeedUrl + [PackagingService.OverrideStagingFeedConfigKey] = invalidFeedUrl }) .Build(); @@ -313,7 +469,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithQualityOverride_ var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = "https://example.com/nuget/v3/index.json", + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json", ["overrideStagingQuality"] = "Prerelease" }) .Build(); @@ -343,7 +499,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithQualityBoth_Uses var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = "https://example.com/nuget/v3/index.json", + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json", ["overrideStagingQuality"] = "Both" }) .Build(); @@ -373,7 +529,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithInvalidQuality_D var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = "https://example.com/nuget/v3/index.json", + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json", ["overrideStagingQuality"] = "InvalidValue" }) .Build(); @@ -403,7 +559,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabledWithoutQualityOverri var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = "https://example.com/nuget/v3/index.json" + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json" }) .Build(); @@ -430,7 +586,7 @@ public async Task NuGetConfigMerger_WhenChannelRequiresGlobalPackagesFolder_Adds var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-48a11dae/nuget/v3/index.json" + [PackagingService.OverrideStagingFeedConfigKey] = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-48a11dae/nuget/v3/index.json" }) .Build(); @@ -488,7 +644,7 @@ public async Task GetChannelsAsync_WhenStagingChannelEnabled_StagingAppearsAfter var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = "https://example.com/nuget/v3/index.json" + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json" }) .Build(); @@ -651,7 +807,7 @@ public async Task GetChannelsAsync_WhenStagingQualityPrerelease_WithFeedOverride .AddInMemoryCollection(new Dictionary { ["overrideStagingQuality"] = "Prerelease", - ["overrideStagingFeed"] = customFeed + [PackagingService.OverrideStagingFeedConfigKey] = customFeed }) .Build(); @@ -791,7 +947,7 @@ public async Task GetChannelsAsync_WhenStagingPinToCliVersionSetButNotSharedFeed var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["overrideStagingFeed"] = "https://example.com/nuget/v3/index.json", + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json", ["stagingPinToCliVersion"] = "true" }) .Build(); diff --git a/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs b/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs index 406e708f0c6..31b8628659c 100644 --- a/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs @@ -423,6 +423,130 @@ public async Task TryCreateTemporaryNuGetConfig_LocalRequested_ReturnsNull_Regar Assert.Null(result); } + [Fact] + public async Task TryCreateTemporaryNuGetConfig_StagingRequested_RefusesWhenPackagingServiceReportsUnavailable() + { + // Regression for radical's review of #17235: on a daily/local/pr CLI the packaging service + // refuses to synthesize a 'staging' channel and surfaces the actionable reason. The bundled + // AppHost restore must not silently fall through to a different feed — it must propagate + // that reason so the user sees the same message the update/new commands now show. + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var executionContext = CreateContextWithIdentityChannel("daily"); + const string unavailableReason = + "Staging unavailable on this daily CLI build. Set overrideStagingFeed or enable the StagingChannelEnabled feature flag to use it."; + var server = CreateServerWithUnavailableStagingChannel(workspace, executionContext, unavailableReason); + + var ex = await Assert.ThrowsAsync( + () => InvokeTryCreateTemporaryNuGetConfigAsync(server, "staging")); + Assert.Equal(unavailableReason, ex.Message); + } + + [Fact] + public async Task GetNuGetSources_StagingRequested_RefusesWhenPackagingServiceReportsUnavailable() + { + // Companion of the TryCreateTemporaryNuGetConfig test above. Without this guard, + // GetNuGetSourcesAsync's "no match -> all explicit channels" fallback hands the + // shared daily feed to nuget restore on a daily-identity CLI even though the project + // pinned channel: staging. + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var executionContext = CreateContextWithIdentityChannel("daily"); + const string unavailableReason = + "Staging unavailable on this daily CLI build. Set overrideStagingFeed or enable the StagingChannelEnabled feature flag to use it."; + var server = CreateServerWithUnavailableStagingChannel(workspace, executionContext, unavailableReason); + + var ex = await Assert.ThrowsAsync( + () => server.GetNuGetSourcesAsync("staging", CancellationToken.None)); + Assert.Equal(unavailableReason, ex.Message); + } + + [Fact] + public async Task GetNuGetSources_NonStagingRequest_NotAffectedByStagingUnavailableReason() + { + // Negative control: the staging refusal must only fire for requestedChannel == "staging". + // A request for any other channel name must continue to resolve normally even when the + // packaging service is reporting staging-unavailable. + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var executionContext = CreateContextWithIdentityChannel("daily"); + var mappings = new[] + { + new PackageMapping(PackageMapping.AllPackages, "https://pkgs.dev.azure.com/fake/v3/index.json") + }; + var dailyChannel = PackageChannel.CreateExplicitChannel( + "daily", PackageChannelQuality.Both, mappings, new FakeNuGetPackageCache()); + var packagingService = new TestPackagingService + { + GetChannelsAsyncCallback = _ => Task.FromResult>([dailyChannel]), + GetStagingChannelUnavailableReasonCallback = () => "Staging unavailable" + }; + + var nugetService = new BundleNuGetService( + new NullLayoutDiscovery(), + new LayoutProcessRunner(new TestProcessExecutionFactory()), + new TestFeatures(), + executionContext, + NullLogger.Instance); + + var server = new PrebuiltAppHostServer( + workspace.WorkspaceRoot.FullName, + "test.sock", + new LayoutConfiguration(), + nugetService, + new TestDotNetCliRunner(), + new TestDotNetSdkInstaller(), + packagingService, + executionContext, + NullLogger.Instance); + + var sources = await server.GetNuGetSourcesAsync("daily", CancellationToken.None); + + Assert.NotNull(sources); + Assert.Contains("https://pkgs.dev.azure.com/fake/v3/index.json", sources); + } + + private static PrebuiltAppHostServer CreateServerWithUnavailableStagingChannel( + TemporaryWorkspace workspace, + CliExecutionContext executionContext, + string unavailableReason) + { + // Mirrors what PackagingService does on a daily/local/pr CLI: omits 'staging' from + // GetChannelsAsync and surfaces the actionable reason via GetStagingChannelUnavailableReason. + // We hand back the 'daily' channel because that is the shared explicit channel the + // pre-fix fallback path would have silently picked up. + var mappings = new[] + { + new PackageMapping(PackageMapping.AllPackages, "https://pkgs.dev.azure.com/fake/v3/index.json") + }; + var dailyChannel = PackageChannel.CreateExplicitChannel( + "daily", PackageChannelQuality.Both, mappings, new FakeNuGetPackageCache()); + + var packagingService = new TestPackagingService + { + GetChannelsAsyncCallback = _ => Task.FromResult>([dailyChannel]), + GetStagingChannelUnavailableReasonCallback = () => unavailableReason + }; + + var nugetService = new BundleNuGetService( + new NullLayoutDiscovery(), + new LayoutProcessRunner(new TestProcessExecutionFactory()), + new TestFeatures(), + executionContext, + NullLogger.Instance); + + return new PrebuiltAppHostServer( + workspace.WorkspaceRoot.FullName, + "test.sock", + new LayoutConfiguration(), + nugetService, + new TestDotNetCliRunner(), + new TestDotNetSdkInstaller(), + packagingService, + executionContext, + NullLogger.Instance); + } + private static CliExecutionContext CreateContextWithIdentityChannel(string identityChannel) => new(new DirectoryInfo(Path.GetTempPath()), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "hives")), diff --git a/tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs b/tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs index c7d9ee625fc..5f201532301 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs @@ -9,6 +9,14 @@ internal sealed class TestPackagingService : IPackagingService { public Func>>? GetChannelsAsyncCallback { get; set; } + /// + /// Optional callback to control the reason returned by + /// . When (the default), + /// the fake reports staging as available (returns ) so existing tests + /// that don't care about staging gating keep working unchanged. + /// + public Func? GetStagingChannelUnavailableReasonCallback { get; set; } + public Task> GetChannelsAsync(CancellationToken cancellationToken = default, string? requestedChannelName = null) { if (GetChannelsAsyncCallback is not null) @@ -20,4 +28,9 @@ public Task> GetChannelsAsync(CancellationToken canc var testChannel = PackageChannel.CreateImplicitChannel(new FakeNuGetPackageCache()); return Task.FromResult>(new[] { testChannel }); } + + public string? GetStagingChannelUnavailableReason() + { + return GetStagingChannelUnavailableReasonCallback?.Invoke(); + } }