Skip to content

Honor configured channel in 'aspire update'#16716

Open
mitchdenny wants to merge 2 commits intomainfrom
mitchdenny/fix-16650-update-respect-channel-config
Open

Honor configured channel in 'aspire update'#16716
mitchdenny wants to merge 2 commits intomainfrom
mitchdenny/fix-16650-update-respect-channel-config

Conversation

@mitchdenny
Copy link
Copy Markdown
Member

@mitchdenny mitchdenny commented May 4, 2026

Description

Fixes #16650.

aspire update previously only consulted --channel/--quality from the command line. When neither was supplied it dropped straight through to the implicit/default channel (whatever NuGet.config happened to point at), even when the user had explicitly set aspire config set channel staging either locally or globally. The CLI silently used a different channel than the user''s stated intent.

This PR centralizes channel resolution in UpdateCommand.ExecuteAsync to use the documented precedence:

  1. explicit --channel / hidden --quality (CLI)
  2. local app-config channel (aspire.config.json in the project tree)
  3. global config channel (e.g. %APPDATA%\.aspire\settings.json)
  4. interactive channel prompt when appropriate (PR hives present)
  5. implicit/default channel as the documented fallback

Local-vs-global precedence for steps 2 and 3 is preserved through the existing IConfiguration registration order: ConfigurationHelper.RegisterSettingsFiles loads the global settings file before the local one, so IConfigurationService.GetConfigurationAsync("channel", ...) returns the local value when both are set.

aspire update --self retains its existing prompt-and-persist behavior; project updates will now pick up the persisted channel on subsequent runs (closing the inconsistency called out in the issue).

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 16716

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

Then in any Aspire app:

# 1. Set a configured channel (local OR global)
aspire config set channel staging
# or:  aspire config set channel staging --global

# 2. Run update WITHOUT --channel
aspire update

Expected: the CLI uses staging. Previously it would silently fall through to the implicit/default channel. With --channel passed explicitly, behavior is unchanged (CLI still wins). Clearing channel from config and running aspire update again should pick the implicit/default channel.

Automated coverage

tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs adds regressions covering local config, global config, explicit-CLI-wins, and the no-config fallback paths.

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

Copilot AI review requested due to automatic review settings May 4, 2026 05:56
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 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 -- 16716

Or

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

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 aspire update so project updates consult persisted channel configuration instead of always falling through to the implicit/default channel. This fits the CLI’s broader config-driven channel selection behavior used by other commands.

Changes:

  • Centralizes channel resolution in UpdateCommand.ExecuteAsync and adds config lookup after CLI args.
  • Preserves explicit CLI precedence over configured values.
  • Adds regression tests for local config, global config, precedence, fallback, and invalid configured channels.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Aspire.Cli/Commands/UpdateCommand.cs Adds config-based channel resolution to project update flow.
tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs Adds unit tests for configured-channel update scenarios.

Comment on lines +160 to +171
// 2. local app config "channel"
// 3. global config "channel"
// 4. interactive channel prompt when appropriate (PR hives present)
// 5. implicit/default channel as the documented fallback
// Local-vs-global precedence for steps 2 and 3 is honored implicitly:
// ConfigurationHelper.RegisterSettingsFiles loads the global settings file before the
// local one, so IConfiguration (and therefore GetConfigurationAsync) returns local first.
var channelName = parseResult.GetValue(_channelOption) ?? parseResult.GetValue(_qualityOption);
var channelFromConfig = false;
if (string.IsNullOrWhiteSpace(channelName))
{
channelName = await _configurationService.GetConfigurationAsync("channel", cancellationToken);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — confirmed and fixed in 46c5979.

IConfiguration is built once at startup via ConfigurationHelper.RegisterSettingsFiles(builder.Configuration, Environment.CurrentDirectory, globalSettingsFile) (Program.cs:295), so reading channel from it always anchored to the launch cwd, not the resolved AppHost project's tree. aspire update --apphost /elsewhere/AppHost.csproj would silently pick up channel from whichever aspire.config.json happened to live in the caller's cwd hierarchy.

Added IConfigurationService.GetConfigurationFromDirectoryAsync(key, startDirectory, ct) which:

  1. walks up from startDirectory for the nearest aspire.config.json (or legacy .aspire/settings.json) using ConfigurationHelper.FindNearestConfigFilePath,
  2. falls back to the global settings file (globalSettingsFile.FullName) if the local file is absent or doesn't contain the key,
  3. never consults the process-wide IConfiguration, so the lookup is no longer anchored to the launch cwd, and
  4. throws InvalidOperationException with ErrorStrings.InvalidJsonInConfigFile on JSON parse failure to match ConfigurationHelper.AddSettingsFile's startup-time behavior — keeping malformed-config diagnostics consistent across code paths.

UpdateCommand now passes projectFile.Directory (falling back to ExecutionContext.WorkingDirectory for safety) so the lookup is scoped to the resolved AppHost project's tree.

Three new tests in UpdateCommandTests cover the regression and the precedence rules (project-local-only, project-local-vs-cwd, project-local-without-channel-falls-back-to-global). All pass; existing UpdateCommandTests still pass (39/39).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

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.

mitchdenny added a commit that referenced this pull request May 4, 2026
…rectory

Addresses Copilot reviewer feedback on PR #16716. The previous fix moved
channel resolution to read from `IConfiguration`, but `IConfiguration` is
rooted at `Environment.CurrentDirectory` at startup via
`ConfigurationHelper.RegisterSettingsFiles`. That means
`aspire update --apphost <path-to-other-app>/AppHost.csproj` ignored the
target app's local `aspire.config.json` and read config from the caller's
cwd tree instead, so the documented "local app-config in the project tree"
precedence was still broken for explicit `--apphost` updates.

Add `IConfigurationService.GetConfigurationFromDirectoryAsync(key, startDirectory)`
which walks up from a caller-supplied directory for the nearest
`aspire.config.json`, then falls back to the global settings file. The
process-wide IConfiguration is intentionally not consulted, so the lookup
is never anchored to the launch cwd.

`UpdateCommand` now passes `projectFile.Directory` to scope the channel
lookup to the resolved AppHost project's tree.

Three new tests cover:
- Project in another directory uses its own local config
- Project-local config wins over cwd config
- Project-local config without `channel` falls back to global

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

github-actions Bot commented May 4, 2026

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.

@joperezr
Copy link
Copy Markdown
Member

joperezr commented May 4, 2026

When neither was supplied it dropped straight through to the implicit/default channel

Is this true? I could've sworn being asked which channel I wanted to use when running aspire update before.

@mitchdenny
Copy link
Copy Markdown
Member Author

The behavior has shifted a bit as folks have been tweaking the experience around working with channels. The general trend is towards asking less questions.

mitchdenny and others added 2 commits May 5, 2026 15:29
'aspire update' previously consulted only the explicit --channel/--quality
option and otherwise silently selected the implicit channel (or prompted only
if PR hives were present). The local and global 'channel' configuration values
were never read, so a user who ran 'aspire config set channel staging' (or had
it saved by 'aspire update --self') would still get the implicit/NuGet-config
based channel on subsequent 'aspire update' runs.

UpdateCommand.ExecuteAsync now resolves the channel using the documented
precedence:

  1. explicit --channel / hidden --quality
  2. local app config 'channel' (aspire.config.json / .aspire/settings.json)
  3. global config 'channel' (~/.aspire/settings.global.json)
  4. interactive channel prompt when PR hives are present
  5. implicit/default channel as the documented fallback

Local-vs-global precedence comes for free because RegisterSettingsFiles loads
the global settings file before the local one, so IConfiguration (and
IConfigurationService.GetConfigurationAsync) returns the local value when both
exist. A configured channel that does not match any available channel now
surfaces a ChannelNotFoundException instead of being silently ignored.

The existing UpdateCommand_WithoutHives_UsesImplicitChannelWithoutPrompting
test is preserved (no configured channel still falls back to the implicit
channel) and joined by new regression coverage:

  * UpdateCommand_LocalConfiguredChannel_IsUsed
  * UpdateCommand_GlobalConfiguredChannel_IsUsed
  * UpdateCommand_ExplicitChannelOverridesConfiguredChannel
  * UpdateCommand_LocalConfiguredChannel_OverridesGlobalConfiguredChannel
  * UpdateCommand_WithoutHives_ConfiguredChannel_TakesPrecedenceOverImplicitFallback
  * UpdateCommand_ConfiguredChannelNotInChannelList_ThrowsChannelNotFound

Fixes #16650

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

Addresses Copilot reviewer feedback on PR #16716. The previous fix moved
channel resolution to read from `IConfiguration`, but `IConfiguration` is
rooted at `Environment.CurrentDirectory` at startup via
`ConfigurationHelper.RegisterSettingsFiles`. That means
`aspire update --apphost <path-to-other-app>/AppHost.csproj` ignored the
target app's local `aspire.config.json` and read config from the caller's
cwd tree instead, so the documented "local app-config in the project tree"
precedence was still broken for explicit `--apphost` updates.

Add `IConfigurationService.GetConfigurationFromDirectoryAsync(key, startDirectory)`
which walks up from a caller-supplied directory for the nearest
`aspire.config.json`, then falls back to the global settings file. The
process-wide IConfiguration is intentionally not consulted, so the lookup
is never anchored to the launch cwd.

`UpdateCommand` now passes `projectFile.Directory` to scope the channel
lookup to the resolved AppHost project's tree.

Three new tests cover:
- Project in another directory uses its own local config
- Project-local config wins over cwd config
- Project-local config without `channel` falls back to global

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny force-pushed the mitchdenny/fix-16650-update-respect-channel-config branch from 46c5979 to e3bf581 Compare May 5, 2026 05:29
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🎬 CLI E2E Test Recordings — 74 recordings uploaded (commit e3bf581)

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
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ 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
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
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LatestCliCanStartStableChannelAppHost ▶️ View Recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View Recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
OtelLogsReturnsStructuredLogsFromStarterAppCore ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ 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

📹 Recordings uploaded automatically from CI run #25359562337

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 CLI update ignores configured channel and silently falls back to implicit NuGet.config channel

3 participants