Skip to content

Eagerly migrate legacy .aspire/settings.json to aspire.config.json on CLI startup#17234

Merged
mitchdenny merged 4 commits into
mainfrom
mitchdenny/eager-migrate-aspire-config-on-startup
May 19, 2026
Merged

Eagerly migrate legacy .aspire/settings.json to aspire.config.json on CLI startup#17234
mitchdenny merged 4 commits into
mainfrom
mitchdenny/eager-migrate-aspire-config-on-startup

Conversation

@mitchdenny
Copy link
Copy Markdown
Member

Description

When a user upgrades to CLI 13.2+ and runs it against an existing AppHost workspace that only has the legacy .aspire/settings.json, the workspace was never migrated to aspire.config.json unless they ran a command that actively wrote settings (run, add, update, pipeline). Read-only commands like aspire doctor, aspire ls, or aspire --version left the workspace stuck on the legacy layout indefinitely, because ProjectLocator only triggers migration when called with createSettingsFile: true.

This change moves migration up to the single CLI startup chokepoint, ConfigurationHelper.RegisterSettingsFiles, so it runs unconditionally for any command. The implementation mirrors the existing global-config migration in Program.GetGlobalSettingsPath and reuses AspireConfigFile.LoadOrCreate, which already contains the legacy-read / new-write logic.

A LegacySettingsFileHasMigratableData guard prevents spurious migrations: it requires the legacy file to contain an appHostPath before migrating. This avoids materializing empty aspire.config.json files alongside the {} directory-walk stop markers that test infrastructure (and some user setups) leave behind. The check uses JsonDocument directly rather than the strict-typed AspireJsonConfiguration deserializer so loosely-typed values in pre-normalized files (e.g. a string "false" inside a Dictionary<string, bool>) don't throw and skip migration. Migration failures fall back to registering the legacy path so the CLI still starts.

Three new tests cover the behavior: the original repro (legacy-only workspace migrates on startup), no-clobber (existing aspire.config.json is preserved), and graceful fallback on malformed JSON.

Fixes: #15488

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
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

Fixes #15488. When the CLI runs against an existing AppHost workspace
that only has the legacy .aspire/settings.json, ConfigurationHelper.
RegisterSettingsFiles now migrates it to aspire.config.json at startup
via AspireConfigFile.LoadOrCreate, mirroring the existing global-config
migration in Program.GetGlobalSettingsPath.

Previously, migration only happened when a command actively wrote
settings (createSettingsFile: true in ProjectLocator), so read-only
commands such as 'aspire doctor', 'aspire ls', or 'aspire --version'
left workspaces stuck on the legacy layout indefinitely.

A LegacySettingsFileHasMigratableData guard requires the legacy file to
carry an appHostPath before migrating, so empty seed files (e.g. the
{} marker that TemporaryWorkspace writes to stop directory walks) do
not produce spurious aspire.config.json files. Migration failures fall
back to registering the legacy path so the CLI still starts.

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 -- 17234

Or

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

Plumb the startup ILogger from Program.BuildApplicationAsync through
ConfigurationHelper.RegisterSettingsFiles so the eager legacy migration
emits an Information entry on success and a Warning (with exception)
on failure. Mirrors the logging pattern used by GetGlobalSettingsPath
for the global-config migration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny marked this pull request as ready for review May 19, 2026 02:19
@mitchdenny mitchdenny requested a review from JamesNK as a code owner May 19, 2026 02:19
Copilot AI review requested due to automatic review settings May 19, 2026 02:19
@mitchdenny mitchdenny requested a review from davidfowl as a code owner May 19, 2026 02:19
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

This PR ensures legacy workspaces that still only have .aspire/settings.json are migrated to aspire.config.json during CLI startup (via ConfigurationHelper.RegisterSettingsFiles), so even read-only commands advance the workspace layout and avoid getting “stuck” on the legacy format.

Changes:

  • Eagerly migrates legacy .aspire/settings.json to aspire.config.json on startup when the legacy file appears to contain real AppHost data.
  • Adds best-effort migration with logging + fallback to using the legacy file when migration can’t be performed.
  • Adds unit tests covering eager migration, no-clobber behavior when aspire.config.json already exists, and malformed legacy JSON behavior.
Show a summary per file
File Description
tests/Aspire.Cli.Tests/Configuration/ConfigurationHelperTests.cs Adds tests validating eager migration, preservation of existing aspire.config.json, and malformed-legacy fallback behavior.
src/Aspire.Cli/Utils/ConfigurationHelper.cs Implements eager legacy-to-modern config migration during settings-file registration, gated by an appHostPath presence check and with best-effort error handling/logging.
src/Aspire.Cli/Program.cs Passes the startup logger into RegisterSettingsFiles to enable migration diagnostics.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0

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.

Clean implementation — the guard + try/catch fallback pattern handles all edge cases well. 1 minor comment on a test description.

Comment thread tests/Aspire.Cli.Tests/Configuration/ConfigurationHelperTests.cs Outdated
The existing RegisterSettingsFiles_FallsBackToLegacyWhenMigrationFails test
used unparseable JSON, which is rejected by LegacySettingsFileHasMigratableData
before migration is attempted, so it never actually entered the try/catch in
RegisterSettingsFiles. The test still has value (it proves unparseable legacy
files don't crash the CLI), but its comments misdescribed the mechanism.

Renamed it to RegisterSettingsFiles_GuardRejectsUnparseableLegacyFile and
corrected its comments to describe what it actually exercises: the guard's
defense against unparseable JSON.

Added RegisterSettingsFiles_FallsBackToLegacyWhenMigrationLoadThrows which
uses a parseable legacy file with a valid appHostPath (so the guard returns
true) but a string value for the strict Dictionary<string, bool> features
field. That makes AspireConfigFile.LoadOrCreate -> AspireJsonConfiguration.Load
throw inside the try block, genuinely exercising the catch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/Aspire.Cli/Utils/ConfigurationHelper.cs Outdated
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 migration logic, guard implementation, error handling, and test coverage. The two-layer defense (guard rejects unparseable files, catch block handles migration failures) ensures the CLI never crashes on startup. Loop ordering correctly prevents re-migration when aspire.config.json already exists. LGTM.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny enabled auto-merge (squash) May 19, 2026 05:40
@mitchdenny mitchdenny merged commit 40817f4 into main May 19, 2026
299 checks passed
@microsoft-github-policy-service microsoft-github-policy-service Bot added this to the 13.4 milestone May 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 90 passed, 0 failed, 2 unknown (commit 23ff7e8)

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
RunPublishFailureScenarioAsync ▶️ 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 #26077806387

@aspire-repo-bot
Copy link
Copy Markdown
Contributor

⚠️ Documentation drafting was attempted but the draft PR could not be confirmed.

See the workflow run for details: https://github.com/microsoft/aspire/actions/runs/26078776570

Added a bullet to the CLI quality-of-life section of whats-new/aspire-13-4.mdx documenting that legacy .aspire/settings.json workspaces are now migrated to aspire.config.json on any CLI startup (including read-only commands like aspire doctor, aspire ls, and aspire --version), not just on write commands as was previously the case.

  • src/frontend/src/content/docs/whats-new/aspire-13-4.mdx — updated

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.

First time run of 13.2 on an existing C# apphost the aspire.config.json is not created.

3 participants