Skip to content

Refuse 'staging' channel synthesis on daily/local/pr CLI builds#17235

Merged
mitchdenny merged 9 commits into
mainfrom
mitchdenny/fix-staging-channel-on-daily-cli
May 19, 2026
Merged

Refuse 'staging' channel synthesis on daily/local/pr CLI builds#17235
mitchdenny merged 9 commits into
mainfrom
mitchdenny/fix-staging-channel-on-daily-cli

Conversation

@mitchdenny
Copy link
Copy Markdown
Member

Description

Fixes: #16652

On a daily Aspire CLI, aspire update --channel staging silently resolved to daily package versions. The packaging service was synthesizing the staging channel relative to the running CLI build:

  • For stable-quality staging, it built a darc feed URL from the running CLI's commit hash (darc-pub-microsoft-aspire-<hash>). That feed only exists for stable release branch commits, never for daily ones.
  • For prerelease/Both quality staging without an explicit overrideStagingFeed, it fell back to the shared daily feed (pkgs.dev.azure.com/.../dotnet9/...), which contains daily packages.

Either way, a daily CLI's --channel staging ended up resolving daily versions and reporting them as "staging".

Approach

Gate staging-channel synthesis on the CLI's baked identity (CliExecutionContext.IdentityChannel, sourced from [AssemblyMetadata("AspireCliChannel", ...)] and validated by IdentityChannelReader):

  • stable identity: SHA-specific darc-pub-microsoft-aspire-<hash> feed is real, allow synthesis.
  • staging identity: dogfoods staging packages (per Auto-register staging channel for staging CLI #17155), allow synthesis.
  • daily, local, pr-<N> identity: refuse and return a localized reason naming the running identity and pointing at overrideStagingFeed (escape hatch) or installing a staging/stable CLI.

Escape hatches preserved:

  • An explicit overrideStagingFeed configuration entry always allows synthesis regardless of identity.
  • The StagingChannelEnabled feature flag continues to opt in for dev/test scenarios.

UpdateCommand now surfaces the packaging-service-provided reason when --channel staging is requested but refused, instead of the generic No channel found matching 'staging' message that hid the actual recovery action from users.

When staging IS synthesized, the resolved feed URL, quality, and pinned version are logged at Information so users can see what --channel staging actually selected (the "show what was resolved" suggestion from the issue RCA).

Why this differs from the closed PR #16717

The earlier PR keyed availability off the assembly version string (Arcade preview vs 3+ identifier daily) and required a new test seam (internal:packaging:cliVersionForTesting).

This PR keys off CliExecutionContext.IdentityChannel instead. It is deterministic, cleaner, doesn't need a new test seam, and naturally covers local and pr-<N> builds. The signal was already baked into the CLI assembly and validated centrally.

Validation

# On a daily CLI
aspire update --channel staging
# Now exits FailedToUpgradeProject with a localized message naming the daily
# identity and pointing at overrideStagingFeed / installing a staging CLI,
# instead of resolving daily packages.

# Escape hatch still works
aspire config set -g overrideStagingFeed <feed-url>
aspire update --channel staging
# Succeeds; logs "Resolved 'staging' channel: feed=..., quality=..., pinnedVersion=...".

# Default behavior unaffected
aspire update
# Unchanged; only the explicit --channel staging path is touched on daily CLIs.

Stable / staging-identity CLIs are unaffected.

Automated coverage

  • tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs adds the full identity matrix: stable, staging, daily, local, pr-12345, daily + overrideStagingFeed, daily + StagingChannelEnabled feature flag. The previous test that asserted the buggy local-CLI shared-feed behavior is rewritten to assert refusal.
  • tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs adds a regression test verifying that --channel staging surfaces the staging-specific reason via DisplayError and exits FailedToUpgradeProject.
  • TestPackagingService gains a matching fake member that defaults to reporting staging as available, so existing tests stay unchanged.

All 3,217 Aspire.Cli.Tests pass locally.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No

Fixes #16652.

PackagingService synthesized the 'staging' channel from the running CLI's
build context: a stable-quality staging used a SHA-specific darc feed built
from the CLI commit, and a Prerelease/Both staging without an override fell
back to the shared daily feed. On a daily, local, or per-PR CLI neither
produces a real staging feed, so 'aspire update --channel staging' silently
resolved to daily package versions.

Gate staging synthesis on the CLI's baked identity (CliExecutionContext.
IdentityChannel from [AssemblyMetadata("AspireCliChannel", ...)]):
  - stable: SHA-specific darc-pub-microsoft-aspire-<hash> feed exists.
  - staging: dogfoods staging packages (per #17155).
  - daily, local, pr-<N>: refuse and surface a localized reason that names
    the running identity and points at 'overrideStagingFeed' or installing a
    staging CLI as recovery paths.

Escape hatches preserved:
  - 'overrideStagingFeed' configuration always allows synthesis.
  - 'StagingChannelEnabled' feature flag continues to opt in for dev/test.

UpdateCommand surfaces the packaging-service reason when --channel staging
is requested but refused, instead of the generic 'No channel found matching
staging' message that hid the actual fix.

When staging IS synthesized, log the resolved feed URL, quality, and pinned
version at Information so users can see what '--channel staging' actually
selected (the 'show what was resolved' suggestion from the issue RCA).

Adds IPackagingService.GetStagingChannelUnavailableReason() and a matching
member on TestPackagingService that defaults to reporting staging as
available so existing tests stay unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17235

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17235"

…ewCommand

- Cache GetStagingChannelUnavailableReason via Lazy<string?> so the formatted
  reason is computed once per process instead of on every GetChannelsAsync call.
- Emit the refusal warning and resolved-staging info log at most once per
  process using Interlocked.Exchange flags. Many code paths invoke
  GetChannelsAsync repeatedly (NewCommand, IntegrationPackageSearchService,
  NuGetPackagePrefetcher, etc.); without this, a daily CLI with channel:
  staging pinned in aspire.config.json would spam the warning on every command.
- Surface the staging-specific reason in NewCommand's error path so users
  scaffolding a project with channel: staging configured get the same
  actionable message UpdateCommand now produces.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny marked this pull request as ready for review May 19, 2026 02:20
Copilot AI review requested due to automatic review settings May 19, 2026 02:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes #16652: a daily Aspire CLI silently resolved aspire update --channel staging to daily package versions because PackagingService.CreateStagingChannel() synthesized the staging feed from either a non-existent SHA-specific darc feed or the shared daily feed. The PR gates staging-channel synthesis on the CLI's baked IdentityChannel (from [AssemblyMetadata("AspireCliChannel", ...)]) so daily/local/pr-N CLIs refuse staging synthesis and return a localized, actionable reason instead.

Changes:

  • Adds IPackagingService.GetStagingChannelUnavailableReason() and identity-based gating in PackagingService, with cached reason, one-shot refusal/resolution log lines, and overrideStagingFeed / StagingChannelEnabled escape hatches preserved.
  • UpdateCommand and NewCommand surface the packaging-service reason when --channel staging (or configured channel: staging) is requested but refused, instead of the generic "No channel found matching" message.
  • New localized resource string StagingChannelUnavailableOnDailyCli (resx + Designer + all 13 xlf files) and test coverage for the full identity matrix plus an UpdateCommand regression test.
Show a summary per file
File Description
src/Aspire.Cli/Packaging/PackagingService.cs Adds identity-gated staging synthesis, cached refusal reason, and one-shot log lines
src/Aspire.Cli/Commands/UpdateCommand.cs Surfaces staging-specific reason when --channel staging is refused
src/Aspire.Cli/Commands/NewCommand.cs Mirrors the staging-specific error path for aspire new
src/Aspire.Cli/Resources/PackagingStrings.resx New StagingChannelUnavailableOnDailyCli resource string
src/Aspire.Cli/Resources/PackagingStrings.Designer.cs Generated accessor for the new resource
src/Aspire.Cli/Resources/xlf/PackagingStrings.*.xlf (13 files) New trans-units (state="new") for all locales
tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs Identity matrix coverage: stable, daily, local, pr-N, override, feature flag
tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs Regression test that --channel staging surfaces the staging reason
tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs Adds matching GetStagingChannelUnavailableReason fake

Copilot's findings

Files not reviewed (1)
  • src/Aspire.Cli/Resources/PackagingStrings.Designer.cs: Language not supported
  • Files reviewed: 20/21 changed files
  • Comments generated: 0

Comment thread src/Aspire.Cli/Packaging/PackagingService.cs
Copy link
Copy Markdown
Member

@JamesNK JamesNK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the staging-channel gating logic, caching, concurrency guards, error surfacing in UpdateCommand/NewCommand, resource strings, and test coverage. The fix is well-structured and correctly addresses #16652.

1 issue flagged: test coverage gap where the feature flag escape hatch isn't actually exercised in isolation.

Comment thread tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs
Comment thread src/Aspire.Cli/Commands/NewCommand.cs
Comment thread src/Aspire.Cli/Commands/UpdateCommand.cs Outdated
Addresses radical's review feedback on #17235. PrebuiltAppHostServer
(and any sibling paths) previously treated a missing requested channel
as a fallback to "all explicit channels", which silently restored
integration packages from the shared daily feed even when the project
pinned channel: staging. Refuse those calls with the same actionable
GetStagingChannelUnavailableReason() the UpdateCommand/NewCommand paths
now use, so the silent-downgrade hole this PR is meant to close is
closed for the bundled AppHost restore path too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/Aspire.Cli/Packaging/PackagingService.cs Outdated
Comment thread src/Aspire.Cli/Commands/UpdateCommand.cs Outdated
Addresses JamesNK's review feedback on #17235. The original
feature-flag test set overrideStagingFeed for the test-host
workaround, which short-circuited IsStagingChannelSynthesisAllowed
before the feature-flag branch ever ran. Add a separate assertion
that exercises GetStagingChannelUnavailableReason() on a service
whose only opt-in is the StagingChannelEnabled feature flag, so a
regression that removes the feature-flag branch would now be caught.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/Aspire.Cli/Packaging/PackagingService.cs Outdated
Mitch Denny and others added 5 commits May 19, 2026 14:10
Addresses JamesNK's review feedback on #17235. The two remaining inline
English literals in NewCommand.ResolveCliTemplateVersionAsync ("No
package channels are available." and "No channel found matching '{0}'.
Valid options are: {1}.") are now sourced from the resource file the
same way the staging-unavailable message already is, so all three
failure paths are localizable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sons

Addresses JamesNK's review feedback on #17235. Introduces a small
StringComparisons helper so the canonical case-insensitive comparison
rule for package-channel names lives in one place, and switches the
existing call sites in PackagingService, UpdateCommand, NewCommand,
PrebuiltAppHostServer, and KnownFeatures from raw
StringComparison.OrdinalIgnoreCase to StringComparisons.ChannelName.
Behaviour is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous code lazily allocated a Lazy<T> via Interlocked.CompareExchange,
which is redundant since Lazy<T> already provides thread-safe one-time
initialization. Construct the Lazy eagerly in the constructor and let it do
the deferral, per JamesNK's review on #17235.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors the same resource extraction that was done for NewCommand earlier
in this PR. Adds NoChannelFoundMatching to UpdateCommandStrings.resx +
Designer, regenerates xlf for all locales, and switches the
ChannelNotFoundException construction site to string.Format against the
resource. Per JamesNK on #17235.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per JamesNK on #17235: replace the duplicated "overrideStagingFeed"
literal in PackagingService (and the test fixtures) with
PackagingService.OverrideStagingFeedConfigKey so a single source of
truth governs the configuration name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Member

@JamesNK JamesNK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found. The fix correctly gates staging-channel synthesis on the CLI's baked identity channel, surfaces actionable error messages through all affected code paths (UpdateCommand, NewCommand, PrebuiltAppHostServer bundled restore), and preserves the existing escape hatches. Test coverage is comprehensive.

@mitchdenny mitchdenny merged commit 3b56f2c into main May 19, 2026
590 of 593 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 90 passed, 0 failed, 1 unknown (commit 94bd1af)

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View recording
AddPackageWhileAppHostRunningDetached ▶️ View recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View recording
AgentInitCommand_DefaultSelection_InstallsDefaultSkills ▶️ View recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View recording
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost ▶️ View recording
AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAndPreservesFiles ▶️ View recording
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive ▶️ View recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View recording
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent ▶️ View recording
Banner_DisplayedOnFirstRun ▶️ View recording
Banner_DisplayedWithExplicitFlag ▶️ View recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View recording
CertificatesClean_RemovesCertificates ▶️ View recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View recording
CreateAndRunAspireStarterProject ▶️ View recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View recording
CreateAndRunEmptyAppHostProject ▶️ View recording
CreateAndRunJavaEmptyAppHostProject ▶️ View recording
CreateAndRunJsReactProject ▶️ View recording
CreateAndRunPythonReactProject ▶️ View recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View recording
CreateAndRunTypeScriptStarterProject ▶️ View recording
CreateJavaAppHostWithViteApp ▶️ View recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View recording
DeployK8sBasicApiService ▶️ View recording
DeployK8sWithExternalHelmChart ▶️ View recording
DeployK8sWithGarnet ▶️ View recording
DeployK8sWithMongoDB ▶️ View recording
DeployK8sWithMySql ▶️ View recording
DeployK8sWithPostgres ▶️ View recording
DeployK8sWithRabbitMQ ▶️ View recording
DeployK8sWithRedis ▶️ View recording
DeployK8sWithSqlServer ▶️ View recording
DeployK8sWithValkey ▶️ View recording
DeployTypeScriptAppToKubernetes ▶️ View recording
DescribeCommandResolvesReplicaNames ▶️ View recording
DescribeCommandShowsRunningResources ▶️ View recording
DetachFormatJsonProducesValidJson ▶️ View recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View recording
DoListStepsShowsPipelineSteps ▶️ View recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View recording
GlobalMigration_PreservesAllValueTypes ▶️ View recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View recording
InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot ▶️ View recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View recording
LatestCliCanStartStableChannelAppHost ▶️ View recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View recording
LogLevelTrace_ProducesTraceEntriesInCliLogFile ▶️ View recording
LogsCommandShowsResourceLogs ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterApp ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterAppIsolated ▶️ View recording
PsCommandListsRunningAppHost ▶️ View recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View recording
ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogContainsEntries ▶️ View recording
ResourceCommand_FailsWhenInteractionServiceIsRequired ▶️ View recording
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput ▶️ View recording
RestoreGeneratesSdkFiles ▶️ View recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View recording
RunReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
RunReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
SecretCrudOnDotNetAppHost ▶️ View recording
SecretCrudOnTypeScriptAppHost ▶️ View recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View recording
StartReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
StartReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
StopAllAppHostsFromAppHostDirectory ▶️ View recording
StopNonInteractiveSingleAppHost ▶️ View recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View recording
UpdateProjectChannelToStable_TypeScript_PicksUpStablePackages ▶️ View recording

📹 Recordings uploaded automatically from CI run #26076137563

@microsoft-github-policy-service microsoft-github-policy-service Bot added this to the 13.4 milestone May 19, 2026
@aspire-repo-bot
Copy link
Copy Markdown
Contributor

✅ No documentation update needed.

Documentation PR could not be created due to a tooling limitation: the create_pull_request tool cannot access commits in the _repos/aspire.dev subdirectory checkout. The required documentation change (updating aspire-update.mdx to document the new --channel staging refusal behavior on daily/local/pr CLI builds and the overrideStagingFeed escape hatch) has been prepared but could not be submitted. A maintainer should manually create a docs PR on microsoft/aspire.dev targeting release/13.4.

radical added a commit to radical/aspire that referenced this pull request May 19, 2026
…spire-empty-typescript-hive

Resolved conflicts in PrebuiltAppHostServer.cs and its tests:

- `GetNuGetSourcesAsync` and `TryCreateTemporaryNuGetConfigAsync`
  visibility widened to `internal` (from microsoft#17235) while keeping this
  branch's `packageSourceOverride` parameter.
- Channel-name comparisons updated to `StringComparisons.ChannelName`
  to match the shared comparer introduced on main.
- Kept both sets of tests in the overlap region: this branch's
  `--source` override tests plus microsoft#17235's staging-unavailable refusal
  tests. Adjusted the two new staging tests that call
  `GetNuGetSourcesAsync` directly to pass `packageSourceOverride: null`
  for the widened signature.

Verified: `dotnet build tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj` succeeds with 0 errors / 0 warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

aspire update --channel staging can select daily package versions from a daily CLI

4 participants