Skip to content

Add automatic background CLI auto-update#14826

Closed
davidfowl wants to merge 6 commits intorelease/13.2from
davidfowl/autoupdate
Closed

Add automatic background CLI auto-update#14826
davidfowl wants to merge 6 commits intorelease/13.2from
davidfowl/autoupdate

Conversation

@davidfowl
Copy link
Copy Markdown
Contributor

Description

Adds automatic background CLI auto-update using a staged approach inspired by copilot-agent-runtime.

Phase 1 — Background download (during command execution):

  • NuGetPackagePrefetcher detects a newer version via existing NuGet metadata check
  • If ShouldAutoUpdate() passes, spawns a detached background process (aspire internal-auto-update) to download and stage the new CLI binary to ~/.aspire/staging/

Phase 2 — Apply on next startup (very early in Program.Main, before DI):

  • Checks ~/.aspire/staging/ for a staged binary
  • Renames current exe to .old.{timestamp} (Windows locked file workaround)
  • Copies staged exe to current location
  • Shows "🚀 Auto-updated to version X.Y.Z" at end of command

Skip conditions (auto-update does NOT run when):

  • Running in CI environments
  • Running as a dotnet tool (dotnet tool update should be used instead)
  • ASPIRE_CLI_AUTO_UPDATE=false env var is set
  • features.autoUpdateEnabled config flag is disabled (aspire config set features.autoUpdateEnabled false)
  • Running aspire update command (avoid double-update)
  • Staging directory already has a pending update

Key design decisions vs previous attempt (#14107):

  • No new abstractions (CliInstaller, CliPlatformDetector, etc.)
  • Staged approach avoids race conditions with running DCP/managed processes
  • Detached background process means short commands don't kill the download
  • ~670 lines vs ~1100 lines in the previous PR

Fixes # (issue)

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?

Adds a staged background auto-update mechanism for the Aspire CLI:

- Phase 1: On startup, NuGetPackagePrefetcher detects newer version and
  spawns a detached background process to download and stage the new CLI
  binary to ~/.aspire/staging/

- Phase 2: On next CLI startup, before DI setup, check staging dir and
  swap the binary using the rename-to-.old trick for Windows locked files.
  Show notification at end of command.

Skip conditions: CI environments, dotnet tool installs, ASPIRE_CLI_AUTO_UPDATE=false
env var, features.autoUpdateEnabled config flag, update command.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 1, 2026 16:37
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 1, 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/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14826

Or

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

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

Adds a staged, background auto-update mechanism for the Aspire CLI: a background process downloads and stages a newer CLI binary, and the next CLI startup applies the staged update before normal execution begins.

Changes:

  • Introduces AutoUpdater to stage downloads and apply updates early on next startup.
  • Hooks update staging into the existing NuGet metadata prefetch/update-check flow.
  • Adds feature flag + CI detection plumbing and unit tests around auto-update gating.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/Aspire.Cli.Tests/Utils/AutoUpdaterTests.cs Adds unit tests for auto-update gating and staging helpers.
tests/Aspire.Cli.Tests/TestServices/TestCliUpdateNotifier.cs Updates test notifier to satisfy the expanded update-notifier contract.
tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs Updates test host-environment fake for new CI detection property.
src/Aspire.Cli/Utils/CliUpdateNotifier.cs Exposes the newer-version string for auto-update staging.
src/Aspire.Cli/Utils/CliHostEnvironment.cs Adds IsRunningInCI detection to gate auto-updates in CI.
src/Aspire.Cli/Utils/AutoUpdater.cs Implements staging directory logic, background download, checksum validation, and apply-on-startup behavior.
src/Aspire.Cli/Program.cs Handles the internal auto-update command and applies staged updates at startup.
src/Aspire.Cli/NuGet/NuGetPackagePrefetcher.cs Triggers background auto-update when a newer CLI version is detected.
src/Aspire.Cli/KnownFeatures.cs Adds features.autoUpdateEnabled feature flag metadata.
src/Aspire.Cli/Commands/BaseCommand.cs Displays a post-command notification when an auto-update was applied at startup.

Comment on lines +166 to +169
// Stage the new binary
Directory.CreateDirectory(stagingDir);
File.Copy(newExePath, Path.Combine(stagingDir, exeName), overwrite: true);

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

SilentDownloadAndStageAsync can be invoked concurrently (multiple CLI processes may spawn internal-auto-update before anything is staged). Writing directly to the final staging path with overwrite can lead to races or partially-written binaries. Consider using a cross-process lock (mutex/lock file) and/or stage to a temp file + atomic move/replace into the staging directory.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +91
public void HasStagedUpdate_ReturnsFalse_WhenNoStagingDirectory()
{
// Staging directory shouldn't exist in the test environment
var result = AutoUpdater.HasStagedUpdate();

// This could be true if a real staging dir exists, but typically it won't in test
// Just verify it doesn't throw
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This test name says it asserts 'ReturnsFalse', but the test doesn't actually assert false (it only asserts the result is a bool) and is dependent on the developer machine having/not having a real ~/.aspire/staging directory. Please make this deterministic (e.g., by redirecting the user profile/home for the test or by creating/removing the staging directory under a temp home) and assert the expected value, or rename the test to reflect what it verifies.

Suggested change
public void HasStagedUpdate_ReturnsFalse_WhenNoStagingDirectory()
{
// Staging directory shouldn't exist in the test environment
var result = AutoUpdater.HasStagedUpdate();
// This could be true if a real staging dir exists, but typically it won't in test
// Just verify it doesn't throw
public void HasStagedUpdate_DoesNotThrow_WhenCheckingStagingDirectory()
{
// Call HasStagedUpdate and ensure it can be invoked without throwing.
var result = AutoUpdater.HasStagedUpdate();
// The return value may vary depending on whether a staging directory exists.
// This test only verifies that a boolean is returned and no exception is thrown.

Copilot uses AI. Check for mistakes.
Comment on lines 67 to 71
);

// Trigger auto-update if an update is available
await TryTriggerAutoUpdateAsync(command, stoppingToken);
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

TryTriggerAutoUpdateAsync is only invoked inside the existing features.IsFeatureEnabled(KnownFeatures.UpdateNotificationsEnabled, true) block. That makes background auto-update implicitly depend on update notifications being enabled, which seems unintended given the separate features.autoUpdateEnabled flag. Consider decoupling the auto-update trigger (and the metadata check it depends on) from the update-notifications feature gate.

See below for a potential fix:

                try
                {
                    await cliUpdateNotifier.CheckForCliUpdatesAsync(
                        workingDirectory: executionContext.WorkingDirectory,
                        cancellationToken: stoppingToken
                        );

                    // Trigger auto-update if an update is available
                    await TryTriggerAutoUpdateAsync(command, stoppingToken);
                }
                catch (System.Exception ex)
                {
                    logger.LogDebug(ex, "Non-fatal error while prefetching CLI packages. This is not critical to the operation of the CLI.");
                }

Copilot uses AI. Check for mistakes.
{
return null;
}

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

TryApplyStagedUpdate() can end up renaming/replacing the wrong executable. In particular, when the CLI is running as a .NET tool, Environment.ProcessPath points to the 'dotnet' host; if a staged update exists, this logic would attempt to move/overwrite the dotnet executable. Please add a guard (e.g., verify Path.GetFileName(currentExePath) matches the expected aspire executable name and/or skip when running as a dotnet tool) before performing any file moves/copies.

Suggested change
// Ensure we are updating the Aspire CLI executable and not the dotnet host or another process.
var currentExeFileName = Path.GetFileName(currentExePath);
if (!string.Equals(currentExeFileName, exeName, StringComparison.OrdinalIgnoreCase))
{
// When running as a dotnet tool, Environment.ProcessPath points to the dotnet host;
// in that case, skip applying the staged update to avoid overwriting the host executable.
return null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +104
var arguments = version is not null
? $"internal-auto-update {cliDownloadBaseUrl} {version}"
: $"internal-auto-update {cliDownloadBaseUrl}";

var psi = new ProcessStartInfo
{
FileName = processPath,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

SpawnBackgroundDownload builds a single command-line string for Arguments, which is fragile if the URL/version ever contain characters that require quoting/escaping. Prefer using ProcessStartInfo.ArgumentList to pass each argument as a separate token (and consider removing stdout/stderr redirection if you truly want a detached fire-and-forget process).

Suggested change
var arguments = version is not null
? $"internal-auto-update {cliDownloadBaseUrl} {version}"
: $"internal-auto-update {cliDownloadBaseUrl}";
var psi = new ProcessStartInfo
{
FileName = processPath,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
var psi = new ProcessStartInfo
{
FileName = processPath,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
psi.ArgumentList.Add("internal-auto-update");
psi.ArgumentList.Add(cliDownloadBaseUrl);
if (version is not null)
{
psi.ArgumentList.Add(version);
}

Copilot uses AI. Check for mistakes.
@davidfowl davidfowl marked this pull request as draft March 1, 2026 17:02
- Use ProcessStartInfo.ArgumentList instead of string concatenation
- Atomic staging: write to temp file then rename to prevent races
- Guard TryApplyStagedUpdate against overwriting dotnet host exe
- Decouple auto-update from update notifications feature gate
- Fix misleading test name for HasStagedUpdate

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

github-actions bot commented Mar 1, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit b200d26:

Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AgentInitCommand_WithMalformedMcpJson_ShowsErrorAndExitsNonZero ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateStartWaitAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording

📹 Recordings uploaded automatically from CI run #22549914767

davidfowl and others added 4 commits March 1, 2026 09:10
The class now handles both NuGet package prefetching and auto-update
triggering, so the name better reflects its broader responsibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace manual ProcessStartInfo with the existing DetachedProcessLauncher
helper which handles platform-specific process detachment correctly
(Windows P/Invoke CreateProcess, Unix pipe handling).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract shared utilities from UpdateCommand, AutoUpdater, and CliDownloader
into a single CliUpdateHelper static class:

- Platform detection (OS + architecture)
- Download with timeout + SHA-512 checksum validation
- Executable replacement with backup-and-swap pattern
- Old backup file cleanup
- IsRunningAsDotNetTool check
- Download URL construction

Removes ~360 lines of duplicated code across the three files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The class was renamed to CliBackgroundService in this PR since it now
handles both NuGet prefetching and CLI auto-updates. Rename the file
to match.

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

@davidfowl what is the state of this one? This is one of the features that would be good to do a lot of dogfood on, so unless it's ready, I'd suggest to re-target to main and do on 13.3

@davidfowl davidfowl closed this Mar 13, 2026
@davidfowl
Copy link
Copy Markdown
Contributor Author

Needs moar security

@dotnet-policy-service dotnet-policy-service bot added this to the 13.2 milestone Mar 13, 2026
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.

3 participants