Skip to content

Fix self-update channel persistence test - #19727

Merged
Karol Zadora-Przylecki (karolz-ms) merged 5 commits into
mainfrom
dev/karolz/fix-19708
Aug 31, 2026
Merged

Karol Zadora-Przylecki (karolz-ms) merged 5 commits into
mainfrom
dev/karolz/fix-19708

Conversation

@karolz-ms

Copy link
Copy Markdown
Contributor

Description

The self-update channel persistence E2E test coupled its sidecar assertion to a live staging package restore. After Aspire 13.5.3 was promoted to GA, the staging alias continued serving that binary while its commit-specific darc feed no longer contained the matching packages. Channel persistence succeeded, but the unrelated project restore failed.

This change refocuses the E2E scenario on its unique contract: after an explicit self-update to staging, the relaunched CLI performs an implicit self-update and reports Updating to channel: staging. The existing hermetic UpdateCommand theory now includes staging so implicit project-update channel selection remains covered without a live package feed.

Validation:

  • Reproduced the original restore failure with Podman.
  • Passed the refocused E2E test 5 consecutive times with Podman.
  • Passed all 4 identity-channel cases in the targeted UpdateCommand theory.

Fixes #19708

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

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7e17da02-39b1-4fa1-ac25-144eb6fcb904
Copilot AI balanced review requested due to automatic review settings August 26, 2026 22:59
@github-actions

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

Or

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

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

Refocuses the channel-persistence regression test to avoid unreliable staging package restores.

Changes:

  • Tests implicit staging self-update after relaunch.
  • Adds staging to hermetic channel-selection coverage.

Reviewed changes

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

File Description
UpdateCommandTests.cs Adds staging identity coverage.
SelfUpdateChannelPersistenceTests.cs Removes project restore and tests implicit self-update.

Comment thread tests/Aspire.Cli.EndToEnd.Tests/SelfUpdateChannelPersistenceTests.cs Outdated
@github-actions

This comment has been minimized.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 26, 2026 23:19

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@mitchdenny

Copy link
Copy Markdown
Member

I investigated #19708 independently in #19731 and landed on the same root cause you did, so treating that as well-corroborated: 13.5.3 was promoted to GA, the staging alias kept serving that binary, but its commit-specific darc feed (darc-pub-microsoft-aspire-b5f14331) no longer carried a matching Aspire.AppHost.Sdk. Channel persistence was working the whole time — the restore is what failed.

I'd like us to take this PR rather than mine. #19731 is the more heavy-handed approach: to make the test fully hermetic it adds an ASPIRE_CLI_STAGING_DOWNLOAD_BASE_URL hook to shipping CliDownloader.cs so the self-update pulls the branch's own binary over loopback. That buys PR-gating of the identity reader path, but it puts a security-sensitive redirect into production code that exists solely to serve a test, and it needed three rounds of hardening (loopback-only, AllowAutoRedirect = false with final-URI revalidation, UseProxy = false). Not worth the ongoing cost. Tests-only is the right call.

One request before it merges, though.

Preserving the test's original intent

As written this PR fixes the restore failure by removing the restore: there's no project creation and no aspire update against a project, so the leg that broke is simply gone. That drops two things the test was originally written to protect:

  1. Assert.Equal("staging", updatedConfig["channel"]) — that the persisted identity channel actually lands in a project's aspire.config.json.
  2. The "AppHost predates the self-update" scenario, which is the shape users actually hit.

The new [InlineData("staging", "staging")] on UpdateCommand_WhenIdentityChannelMatchesRegisteredChannel_UsesItWithoutPrompting is good backfill, but it injects the identity channel rather than proving the sidecar round-trip produces it, so it can't catch a regression between the writer and the reader.

We can keep that coverage and still stay tests-only, by making the restore hermetic instead of deleting it — using the existing packages field on the install sidecar:

Optional path to a flat directory of .nupkg files that the CLI's Aspire* package feed should resolve from directly (the sidecar equivalent of ASPIRE_CLI_PACKAGES). Consumed by PackagingService, which synthesizes a package channel pointing at this directory.
— IInstallSidecarReader.cs:50-56

Point it at the harness's local hive and the darc feed is never consulted, so no future staging promotion can break the test through that path. Critically, InstallSidecarWriter.PrepareForSelfUpdate (InstallSidecarWriter.cs:46-56) rewrites only channel/version/commit and copies every other field through — so packages survives the self-update and is honored by the relaunched binary.

Concretely, two changes to your sidecar block:

     await auto.RunCommandAsync(
         "install_root=$HOME/.aspire-self-update-e2e; " +
+        "packages_dir=$(dirname \"$(find ~/.aspire/hives -type f -name 'Aspire.Hosting.*.nupkg' | head -1)\"); " +
+        "test -n \"$packages_dir\"; " +
         "mkdir -p \"$install_root/bin\"; " +
         "cp \"$(command -v aspire)\" \"$install_root/bin/aspire\"; " +
         "chmod +x \"$install_root/bin/aspire\"; " +
-        "printf '%s\\n' '{\"source\":\"script\",\"channel\":\"stable\"}' > \"$install_root/bin/.aspire-install.json\"; " +
+        "printf '{\"source\":\"script\",\"channel\":\"stable\",\"packages\":\"%s\"}\\n' \"$packages_dir\" > \"$install_root/bin/.aspire-install.json\"; " +
         "export PATH=\"$install_root/bin:$PATH\" ASPIRE_CLI_TELEMETRY_OPTOUT=true; hash -r; " +
         "test \"$(command -v aspire)\" = \"$install_root/bin/aspire\"",
         counter);

plus restoring the project leg (create AppHost, strip the assigned channel, then aspire update --non-interactive --yes and assert the config). The exact block is in #19731 at SelfUpdateChannelPersistenceTests.cs:36-57 and :82-89 if you want to lift it.

Verified

I ran precisely this shape — your hermetic-restore setup, no production changes, project leg retained — twice locally against ASPIRE_E2E_ARCHIVE, both green. The decoded terminal recording confirms the relaunched process is genuinely the published staging binary and not a local no-op:

✅ Updated to version: 13.5.3+b5f143315ffb6968ea939a9978797a5b20e4c688
⚠️ ...emulating identity 'staging' version '13.5.3'... via ... the install sidecar
13.5.3+b5f143315ffb6968ea939a9978797a5b20e4c688      # local build is 13.6.0-local.*

So published staging 13.5.3 does honor packages today — no version-skew concern.

Two things to watch on rebase

  • The quarantine attribute. Quarantine failing staging self-update test #19733 added [QuarantinedTest("https://github.com/microsoft/aspire/issues/19708")] to this test on main after this PR was opened, which is part of why it's showing CONFLICTING. If the conflict resolves toward main, the attribute survives and the fix never actually runs in PR CI. It needs to be dropped (dotnet run --project tools/QuarantineTools -- -u ...) — heads up that the tool leaves a spurious BOM behind; the sibling files are all BOM-free.
  • Residual live dependency. Both aspire update --self calls still download from the real staging endpoint. packages covers NuGet restore only, not the CLI binary download. Fine to accept — just worth being explicit that this doesn't make the test fully hermetic.

Happy to push the combined change to a branch you can cherry-pick if that's easier than lifting it by hand. I'll close #19731 once this lands.

# Conflicts:
#	tests/Aspire.Cli.EndToEnd.Tests/SelfUpdateChannelPersistenceTests.cs
Restores the project-update leg of the test and makes its restore
hermetic via the install sidecar's `packages` field, rather than
removing the step that fails.

Without `packages`, the relaunched CLI derives its Aspire feed from the
identity it just persisted (darc-pub-microsoft-aspire-<commit>). Once
that staging build is promoted to GA the feed stops carrying a matching
Aspire.AppHost.Sdk, which is the reported failure in #19708. Pinning
`packages` at the harness hive removes that dependency, and
InstallSidecarWriter.PrepareForSelfUpdate rewrites only
channel/version/commit, so the field survives the self-update.

This restores two behaviours the test was written to cover: that the
persisted identity channel lands in a project's aspire.config.json, and
the "AppHost predates the self-update" scenario users actually hit.

The second `aspire update --self` is dropped because it cannot coexist
with `packages`: once the sidecar's channel is staging, the synthesized
local-hive channel replaces the same-named built-in channel
(PackagingService.GetChannelsAsync) and a local hive has no
CliDownloadBaseUrl, so self-download fails with "Channel 'staging' does
not support CLI downloads". The project update asserts the same
persistence more strongly, via the resulting config.

Also drops the [QuarantinedTest] attribute added by #19733 while
resolving the merge with main, so the fix actually runs in CI.

Validated: 3 consecutive passing E2E runs against ASPIRE_E2E_ARCHIVE,
with quarantined and outerloop tests excluded. Recording confirms the
relaunched process is the published staging binary
(13.5.3+b5f143315ffb6968ea939a9978797a5b20e4c688), not a local no-op.
UpdateCommandTests theory: 4 passed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f2f02689-8514-4cf8-b38d-f2b62f506754
Copilot AI review requested due to automatic review settings August 28, 2026 04:47
@mitchdenny

Copy link
Copy Markdown
Member

I've pushed the change to this branch (053948a3f5), plus a merge of main that resolves the conflict — the PR is MERGEABLE again. Please review; happy to revert any of it if you'd rather take a different shape.

One correction to my earlier comment. I proposed keeping your second aspire update --self and adding packages. That doesn't work — the two are mutually exclusive by design, which I only found by running it:

🆙 Updating to channel: staging
❌ Failed to update CLI: Channel 'staging' does not support CLI downloads.

The mechanism, from PackagingService.GetChannelsAsync (PackagingService.cs:227-246):

The synthesized channel takes the running CLI's identity-channel name and wins over ANY same-named channel synthesized above [...] because the explicit override is the most specific signal of intent.

So the first --self --channel staging works (sidecar channel is still stable, so the override replaces stable and the built-in staging channel is intact). But that update rewrites the sidecar to channel: staging while preserving packages — so on the next invocation the override replaces staging with a local hive, and a local hive has no CliDownloadBaseUrl. Self-download is off for whatever channel you've pinned packages for.

I dropped the second --self and used the project update as the assertion instead. It proves the same thing more strongly: the relaunched process resolved the persisted identity, evidenced by staging landing in the project's aspire.config.json rather than by a log line. Comment in the test explains why a second --self can't be used, so nobody re-adds it.

What's on the branch now

  • Project creation + channel strip restored, so the AppHost predates the self-update (the shape users hit).
  • Sidecar carries packages pinned at the harness hive, so restore never consults darc-pub-microsoft-aspire-<commit> and a future staging→GA promotion can't break it.
  • Assert.Equal("staging", updatedConfig["channel"]) restored.
  • [QuarantinedTest] from Quarantine failing staging self-update test #19733 dropped during the merge — without this the fix wouldn't have run in CI at all.
  • Your [InlineData("staging", "staging")] kept as-is.
  • Test name back to ...ForImplicitProjectUpdate since the project update is the assertion again.

Validation

3 consecutive passing E2E runs against ASPIRE_E2E_ARCHIVE with quarantined/outerloop excluded, plus UpdateCommandTests theory 4/4. Decoded recording confirms the relaunched process really is the published staging binary and not a local no-op:

✅ Updated to version: 13.5.3+b5f143315ffb6968ea939a9978797a5b20e4c688
⚠️ ...emulating identity 'staging' version '13.5.3'... via ... the install sidecar

(Local build is 13.6.0-local.*, so the swap is real.) Note this branch has zero production diff against main, which is the point — tests-only.

Still worth knowing

One live CLI download remains, from the real staging endpoint. packages covers NuGet restore only, not the CLI binary fetch, so this isn't fully hermetic. #19731 addressed that with an ASPIRE_CLI_STAGING_DOWNLOAD_BASE_URL hook in CliDownloader.cs, but we decided a permanent security-sensitive redirect in shipping code isn't worth it to serve a test. Flagging so the residual exposure is a known, accepted one. I'll close #19731 once this merges.

@github-actions

This comment has been minimized.

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.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread tests/Aspire.Cli.EndToEnd.Tests/SelfUpdateChannelPersistenceTests.cs Outdated
@mitchdenny

Copy link
Copy Markdown
Member

Karol Zadora-Przylecki (@karolz-ms) I made some changes and approved. Leave for you to merge just in case you want to tweak my changes.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Tests selector

2 / 99 PR test projects · 1 PR job · 0 advisory-only targets, from 2 changed files.

Selected PR test projects (2 / 99)

Aspire.Cli.EndToEnd.Tests, Aspire.Cli.Tests

Selected PR jobs (1)

extension-e2e

Advisory workflow impact (0)

none


How these were chosen — grouped by what changed

🧪 tests/Aspire.Cli.EndToEnd.Tests/SelfUpdateChannelPersistenceTests.cs (changed test)
→ 1 directly: Aspire.Cli.EndToEnd.Tests

🧪 tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs (changed test)
→ 1 directly: Aspire.Cli.Tests

Job reasons

Job Triggered by
extension-e2e tests/Aspire.Cli.EndToEnd.Tests/SelfUpdateChannelPersistenceTests.cs, tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs

Selection computed for commit 0113022.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

tests/Aspire.Cli.EndToEnd.Tests/SelfUpdateChannelPersistenceTests.cs:98

  • This does not exercise the contract stated in the PR description (“the relaunched CLI performs an implicit self-update and reports Updating to channel: staging”). The fixture creates a C# AppHost, and TryUpdateCliBeforeGuestProjectUpdateAsync returns immediately for C# projects; the only code that emits that message is ExecuteSelfUpdateAsync. After the preceding screen clear, this command only performs a project package update and the test only checks the persisted project channel. Either use a guest AppHost path that can trigger the implicit CLI update and assert the message, or align the PR description with the project-channel-persistence contract actually tested here.
            "aspire update --non-interactive --yes",

tests/Aspire.Cli.EndToEnd.Tests/SelfUpdateChannelPersistenceTests.cs:71

  • This guard does not make the setup command fail: each fragment is separated by ;, so when find returns nothing, test -n fails but execution continues, dirname "" becomes ., and the final successful path check masks the failure. Chain the setup steps so RunCommandAsync fails immediately instead of writing the workspace as the package override and producing a misleading later restore failure.
            "package_path=$(find ~/.aspire/hives -type f -name 'Aspire.Hosting.*.nupkg' -print -quit); " +
            "test -n \"$package_path\"; " +

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ CI Failure Analysis: Possible Flaky Test(s)

The CI build failed due to test failure(s) that appear unrelated to the PR changes. These may be flaky tests.

Suspected flaky test(s):

  • Aspire.Cli.Tests.Utils.EnvironmentCheckerTests.CheckAllAsync_TimedOutCheckReportsWarningAndContinues in job Tests / Cli / Cli (macos-latest)
    • Error: Assert.Collection() Failure: Item comparison failure
      Collection: [EnvironmentCheckResult { Category = "environment", Details = null, Fix = null, Link = null, Message = "Environment check 'test-environment' timed out aft"..., ... }, EnvironmentCheckResult { Category = "environment", Details = null, Fix = null, Link = null, Message = "Environment check 'test-environment' timed out aft"..., ... }]
      Error: Assert.Same() Failure: Values are not the same instance
      Expected: EnvironmentCheckResult { Category = "environment", Details = null, Fix = null, Link = null, Message = "Completed", ... }
      Actual: EnvironmentCheckResult { Category = "environment", Details = null, Fix = null, Link = null, Message = "Environment check 'test-environment' timed out aft"..., ... }
    • Stack Trace (first frames):
      at Aspire.Cli.Tests.Utils.EnvironmentCheckerTests.CheckAllAsync_TimedOutCheckReportsWarningAndContinues() in /Users/runner/work/aspire/aspire/tests/Aspire.Cli.Tests/Utils/EnvironmentCheckerTests.cs:line 55
      --- End of stack trace from previous location ---
      
    • Why likely flaky: Matches prior known cause 'cli-environmentchecker-timedout-flaky': a timing-sensitive test where a timeout check completes too slowly under CI load, causing a race between expected 'Completed' and actual timeout message. The PR only touched SelfUpdateChannelPersistenceTests.cs and UpdateCommandTests.cs, not EnvironmentCheckerTests.cs or its production code.

Suggested actions:

  • Re-run the failed CI jobs to confirm if the failure is intermittent
  • If the test continues to fail, consider quarantining it using /quarantine-test <test name> <issue URL>
  • Search existing issues to see if this test is already known to be flaky

You can re-run the failed jobs from the workflow run page.

David Pine (IEvangelist) pushed a commit that referenced this pull request Sep 3, 2026
* Fix Issue 19708

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7e17da02-39b1-4fa1-ac25-144eb6fcb904

* Eliminate false positive

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Make the restore hermetic instead of dropping the project update

Restores the project-update leg of the test and makes its restore
hermetic via the install sidecar's `packages` field, rather than
removing the step that fails.

Without `packages`, the relaunched CLI derives its Aspire feed from the
identity it just persisted (darc-pub-microsoft-aspire-<commit>). Once
that staging build is promoted to GA the feed stops carrying a matching
Aspire.AppHost.Sdk, which is the reported failure in #19708. Pinning
`packages` at the harness hive removes that dependency, and
InstallSidecarWriter.PrepareForSelfUpdate rewrites only
channel/version/commit, so the field survives the self-update.

This restores two behaviours the test was written to cover: that the
persisted identity channel lands in a project's aspire.config.json, and
the "AppHost predates the self-update" scenario users actually hit.

The second `aspire update --self` is dropped because it cannot coexist
with `packages`: once the sidecar's channel is staging, the synthesized
local-hive channel replaces the same-named built-in channel
(PackagingService.GetChannelsAsync) and a local hive has no
CliDownloadBaseUrl, so self-download fails with "Channel 'staging' does
not support CLI downloads". The project update asserts the same
persistence more strongly, via the resulting config.

Also drops the [QuarantinedTest] attribute added by #19733 while
resolving the merge with main, so the fix actually runs in CI.

Validated: 3 consecutive passing E2E runs against ASPIRE_E2E_ARCHIVE,
with quarantined and outerloop tests excluded. Recording confirms the
relaunched process is the published staging binary
(13.5.3+b5f143315ffb6968ea939a9978797a5b20e4c688), not a local no-op.
UpdateCommandTests theory: 4 passed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f2f02689-8514-4cf8-b38d-f2b62f506754

* Improve package path processing

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mitch Denny <midenn@microsoft.com>
Copilot-Session: 7e17da02-39b1-4fa1-ac25-144eb6fcb904
Copilot-Session: f2f02689-8514-4cf8-b38d-f2b62f506754
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Failing test]: Aspire.Cli.EndToEnd.Tests.SelfUpdateChannelPersistenceTests.SelfUpdateToStaging\_RelaunchedCliUsesStagingForImplicitProjectUpdate

3 participants