Skip to content

Refuse synthesized 'staging' channel on daily CLI builds - #16717

Closed
Mitch Denny (mitchdenny) wants to merge 5 commits into
mainfrom
mitchdenny/fix-16652-update-channel-staging-daily-cli
Closed

Refuse synthesized 'staging' channel on daily CLI builds#16717
Mitch Denny (mitchdenny) wants to merge 5 commits into
mainfrom
mitchdenny/fix-16652-update-channel-staging-daily-cli

Conversation

@mitchdenny

@mitchdenny Mitch Denny (mitchdenny) commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Fixes #16652.

PackagingService synthesized the staging channel relative to the running CLI build:

  • For stable-quality staging it built a darc feed URL from the current CLI''s commit hash (darc-pub-microsoft-aspire-{currentCliCommitHash}).
  • For prerelease/Both staging quality without an explicit feed override it fell back to the shared daily feed.

When the running CLI is itself a daily build, neither produces a real staging feed: there is no SHA-specific darc feed for daily commits, and the shared daily feed contains daily packages. As a result aspire update --channel staging silently resolved to daily versions — the bug tracked by #16652.

This PR makes staging resolution deterministic on daily CLI builds by refusing to synthesize the channel rather than silently downgrading:

  1. New IPackagingService.GetStagingChannelUnavailableReason() returns a localized, user-facing explanation when staging cannot be resolved.
  2. CreateStagingChannel() consults that helper. If staging is unavailable on a daily CLI it logs a warning and returns null instead of fabricating a daily feed.
  3. UpdateCommand surfaces the same reason in its ChannelNotFoundException message when the user explicitly passes --channel staging, so the failure is clear and actionable.
  4. CreateStagingChannel() always logs the resolved feed URL + quality + pinned version when it does produce a channel (the "show what was resolved" suggestion from the issue).

Escape hatches and unaffected paths:

  • An explicit overrideStagingFeed configuration entry continues to take ownership; staging is allowed in that case regardless of CLI build.
  • Stable releases and "blessed" prereleases (preview.1, rc.1) continue to produce a real staging channel.
  • Only Arcade-style 3+ identifier prerelease versions (e.g. preview.1.26210.1) are treated as daily.
  • Unparseable / null / missing CLI versions are treated as daily (fail-safe, refuses to silently downgrade).

This is a 💥 blocking-release fix targeting main first; a backport PR to release/13.3 will follow.

Validation

To verify locally, install the CLI from this PR:

# Linux/macOS
./eng/scripts/get-aspire-cli-pr.sh 16717

# Windows PowerShell
.\eng\scripts\get-aspire-cli-pr.ps1 16717

Then:

# 1. Confirm a daily CLI now refuses --channel staging
aspire update --channel staging
# Expected: ChannelNotFoundException with the localized
#   "staging channel unavailable on this daily CLI" message,
#   pointing at overrideStagingFeed as the recovery path.

# 2. Confirm overrideStagingFeed still allows it
aspire config set overrideStagingFeed https://pkgs.dev.azure.com/dnceng/...
aspire update --channel staging
# Expected: succeeds, logs "Resolved 'staging' channel: feed=..., quality=..., pinnedVersion=...".

# 3. Confirm channel-less aspire update is unaffected on a daily CLI
aspire config unset overrideStagingFeed
aspire update
# Expected: unchanged behavior; only the explicit --channel staging path is touched.

A stable / blessed-preview CLI is unaffected — staging continues to resolve via the SHA-specific darc feed.

Automated coverage

  • tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs (new file, ~280 lines) covers staging behavior across stable / blessed-preview / daily CLI versions, with and without overrideStagingFeed, including the unparseable-version fail-safe.
  • tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs covers --channel staging surfacing the staging-specific exception message instead of the generic "no channel matching" message.
  • A test seam (internal:packaging:cliVersionForTesting config key) lets unit tests deterministically simulate stable, blessed-preview, and daily CLI builds without depending on the actual Aspire.Cli.dll assembly version. The key is internal/test-only, scoped to PackagingConfigurationKeys, and clearly documented as such.

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
  • Does the change require an update in our Aspire docs?
    • Yes
    • No

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🚀 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 -- 16717

Or

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Prevents aspire update --channel staging from silently resolving to daily packages when the running CLI is itself a daily build. The PR adds staging-unavailable detection in PackagingService, reuses that reason in UpdateCommand, and adds regression tests plus localized resource entries.

Changes:

  • Added a new staging-unavailable reason API and daily-build heuristic in PackagingService.
  • Updated UpdateCommand to surface a staging-specific error instead of the generic missing-channel message.
  • Added regression tests and new packaging resource strings for the user-facing message.

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Aspire.Cli.Tests/TestServices/TestPackagingService.cs Extended test fake to expose staging-unavailable reason.
tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs Added regression coverage for daily/stable/blessed staging resolution cases.
tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs Added test for explicit staging error message.
src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hant.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.zh-Hans.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.tr.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.ru.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.pt-BR.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.pl.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.ko.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.ja.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.it.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.fr.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.es.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.de.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/xlf/PackagingStrings.cs.xlf Added localized staging-unavailable string entry.
src/Aspire.Cli/Resources/PackagingStrings.resx Added source resource string for staging-unavailable message.
src/Aspire.Cli/Resources/PackagingStrings.Designer.cs Regenerated strongly-typed resource accessor.
src/Aspire.Cli/Packaging/PackagingService.cs Added daily-build gating, test seam, logging, and staging-unavailable reason generation.
src/Aspire.Cli/Commands/UpdateCommand.cs Switched missing-channel message construction to use staging-specific reason.
Files not reviewed (1)
  • src/Aspire.Cli/Resources/PackagingStrings.Designer.cs: Language not supported

Comment thread src/Aspire.Cli/Packaging/PackagingService.cs
Comment thread src/Aspire.Cli/Packaging/PackagingService.cs
Comment thread src/Aspire.Cli/Packaging/PackagingService.cs Outdated
Comment thread src/Aspire.Cli/Commands/UpdateCommand.cs
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 2 jobs were identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

@mitchdenny
Mitch Denny (mitchdenny) force-pushed the mitchdenny/fix-16652-update-channel-staging-daily-cli branch from 560b48f to 88af838 Compare May 5, 2026 22:57
@mitchdenny Mitch Denny (mitchdenny) added this to the 13.4 milestone May 6, 2026
@mitchdenny
Mitch Denny (mitchdenny) force-pushed the mitchdenny/fix-16652-update-channel-staging-daily-cli branch from 442c76a to c4776de Compare May 15, 2026 03:48
Mitch Denny (mitchdenny) pushed a commit that referenced this pull request May 15, 2026
Three issues raised in the Copilot review of #16717:

1. Asymmetric overrideStagingFeed validation. GetStagingChannelUnavailableReason
   short-circuited on any non-empty override, but GetStagingFeedUrl only honors
   it when it parses as an HTTP(S) URL via UrlHelper.IsHttpUrl. Setting
   overrideStagingFeed=foo (any non-URL) bypassed the daily-CLI guard, then the
   feed-URL builder silently fell back to the shared dotnet9 daily feed —
   reintroducing #16652. Match the validation in both call sites.

2. Test seam exposed via aspire config. The
   internal:packaging:cliVersionForTesting key was readable from IConfiguration,
   which aspire config set writes to (only legacy AppHost keys were blocked).
   Users could spoof CLI version with
   'aspire config set internal:packaging:cliVersionForTesting 13.4.0' to bypass
   the daily-CLI guard in production. Add IsInternalConfigurationKey to
   AppHostPathConfigurationPolicy and reject internal: keys from both
   'config set' and 'config delete'. Tests still drive the seam through
   IConfiguration directly so they are unaffected.

3. Self-update path. 'aspire update --self --channel staging' goes through
   CliDownloader.DownloadLatestCliAsync rather than ChannelNotFoundException, so
   it surfaced a generic 'Unsupported channel' error instead of the new staging
   reason. Consult GetStagingChannelUnavailableReason in ExecuteSelfUpdateAsync
   before any download work.

Tests: 3036 passed / 0 failed / 20 skipped on Aspire.Cli.Tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mitch Denny (mitchdenny) and others added 4 commits May 16, 2026 13:01
On a daily Aspire CLI, `aspire update --channel staging` could resolve to
daily packages because PackagingService.CreateStagingChannel() either built a
per-commit darc-pub feed URL from the daily commit hash (no such feed exists)
or fell back to the shared dotnet9 daily feed. Either way the user got daily
packages instead of staging packages with no warning.

PackagingService now refuses to synthesize the staging channel when the
running CLI is itself a daily/CI build (detected via prerelease-identifier
count on the assembly informational version) unless the user provides an
explicit overrideStagingFeed configuration value. The resolved staging feed
URL is also logged at info level so users can verify channel resolution.

When the staging channel is omitted on a daily CLI, `aspire update --channel
staging` now surfaces an actionable error explaining why and how to recover
(set 'overrideStagingFeed' or use a stable CLI build).

Fixes #16652

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

The 8 pre-existing tests in PackagingServiceTests that exercise the
shared-feed and pin-to-CLI-version paths assume the running CLI is not
itself a daily/CI build, but they don't pin the version. With this PR's
new daily-CLI guard, when CI builds the test assembly with a
daily-flavored VersionSuffix (e.g. pr.NNNN.gSHA), the daily heuristic
treats the running CLI as daily and GetStagingChannelUnavailableReason
hides the staging channel, causing the tests to fail with "Sequence
contains no matching element" on .First(c => c.Name == "staging").

Use the existing internal:packaging:cliVersionForTesting test seam to
pin a stable CLI version (13.4.0) in those tests so the daily-CLI guard
doesn't trigger and the tests deterministically exercise the shared-feed
and pinning code paths regardless of the test-assembly's build flavor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Three issues raised in the Copilot review of #16717:

1. Asymmetric overrideStagingFeed validation. GetStagingChannelUnavailableReason
   short-circuited on any non-empty override, but GetStagingFeedUrl only honors
   it when it parses as an HTTP(S) URL via UrlHelper.IsHttpUrl. Setting
   overrideStagingFeed=foo (any non-URL) bypassed the daily-CLI guard, then the
   feed-URL builder silently fell back to the shared dotnet9 daily feed —
   reintroducing #16652. Match the validation in both call sites.

2. Test seam exposed via aspire config. The
   internal:packaging:cliVersionForTesting key was readable from IConfiguration,
   which aspire config set writes to (only legacy AppHost keys were blocked).
   Users could spoof CLI version with
   'aspire config set internal:packaging:cliVersionForTesting 13.4.0' to bypass
   the daily-CLI guard in production. Add IsInternalConfigurationKey to
   AppHostPathConfigurationPolicy and reject internal: keys from both
   'config set' and 'config delete'. Tests still drive the seam through
   IConfiguration directly so they are unaffected.

3. Self-update path. 'aspire update --self --channel staging' goes through
   CliDownloader.DownloadLatestCliAsync rather than ChannelNotFoundException, so
   it surfaced a generic 'Unsupported channel' error instead of the new staging
   reason. Consult GetStagingChannelUnavailableReason in ExecuteSelfUpdateAsync
   before any download work.

Tests: 3036 passed / 0 failed / 20 skipped on Aspire.Cli.Tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny
Mitch Denny (mitchdenny) force-pushed the mitchdenny/fix-16652-update-channel-staging-daily-cli branch from 7f869dd to 3c78194 Compare May 16, 2026 03:02
@github-actions

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

  • Tests / Hosting-1 / Hosting-1 (8-core-ubuntu-latest) - Failed step 'Build test project' will be retried because the job log shows a likely transient infrastructure network failure. Matched pattern: /Unable to load the service index for source https:\/\/(?:pkgs\.dev\.azure\.com\/dnceng|dnceng\.pkgs\.visualstudio\.com)\/public\/_packaging\//i.

@davidfowl

Copy link
Copy Markdown
Collaborator

pr-testing skill?

Removes the AppHostPathConfigurationPolicy.IsInternalConfigurationKey gate,
the associated ErrorStrings resource (plus xlf entries), and the two
ConfigCommandTests cases. The staging-channel daily-CLI guard and the
override-feed validation (#1) and self-update short-circuit (#3) from
commit 3c78194 are preserved.

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

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

Matched test failure patterns (1 test)
  • Aspire.Cli.EndToEnd.Tests.KubernetesDeployWithValkeyTests.DeployK8sWithValkey — Unable to access container registry during publish

@github-actions

Copy link
Copy Markdown
Contributor

CLI E2E Tests failed — 84 passed, 1 failed, 1 unknown (commit 8098c91)

Failed Tests

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View recording
AddPackageWhileAppHostRunningDetached ▶️ View recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ 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 failure 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_FailsWhenInteractionServiceIsRequired ▶️ View recording
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput ▶️ View recording
RestoreGeneratesSdkFiles ▶️ View recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View recording
SecretCrudOnDotNetAppHost ▶️ View recording
SecretCrudOnTypeScriptAppHost ▶️ View recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ 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 #25956711745

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

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