Network-aware migration channel resolution for `dotnetup`
- Dominant language
- C#
- Stars
- 3.2k
- Forks
- 1.3k
- PR merge metrics
- PR metrics pending
Description
### Prerequisites
- [x] I have read the [dotnetup documentation](https://github.com/dotnet/sdk/tree/release/dnup/documentation/general/dotnetup).
- [x] I have searched for [existing dotnetup issues](https://github.com/dotnet/sdk/issues?q=is%3Aissue%20state%3Aopen%20label%3Adotnetup).
- [x] I have searched for [existing dotnetup discussions](https://github.com/dotnet/sdk/discussions/categories/dotnetup).
### Issue type
Bug report
### Description
Network-aware migration channel resolution for `dotnetup`
## Summary
Today, when `dotnetup init` migrates a pre-existing **system** SDK/runtime install,
`MigrationWorkflow.BuildMigrationSelections` maps the installed version to a channel
**purely syntactically and offline** via `DotnetupUtilities.VersionToPatchBasedChannel`:
- SDK with `Patch >= 100` → bare feature band `{major}.{minor}.{band}xx` (prerelease dropped)
- otherwise → `{major}.{minor}`
This is fast and air-gap-safe, but it cannot answer two questions that require knowledge
of what is actually published:
1. **Is the installed version still the "latest" for its grouping?**
- `major.minor.{band}xx` for SDKs, `major.minor.xx` for runtimes.
2. **Is the installed version available from the signed releases manifest at all, or only
from the daily/blob feed?** (i.e. should the recorded channel be a `-daily` channel?)
## Motivation
There is a real window where a preview build exists **only** as a daily/blob-feed build
before it is listed in the signed releases manifest. For example, `.NET 11 preview 5`
(`11.0.100-preview.5`) was downloadable from the daily SDK build table on **2026-05-24**
(dotnet/dotnet#6809) but did not appear in `release-notes/11.0/releases.json` until
**2026-06-09**. During that window the *only* way to have that SDK on disk was a daily
build — so the consumer had effectively opted into the `11.0.1xx` daily channel.
Mapping such an install to the bare band `11.0.1xx` is wrong during that window: the band
does not resolve from the signed manifest yet, so the migration cannot install/track it.
Conversely, two different intents must be distinguished:
- **Latest version of a grouping** (e.g. `9.0.1xx` is the newest 9.0.1xx SDK). The user (or
Windows Update / another system installer) is tracking "latest of this band"; a
roll-forward band channel is the correct recorded intent.
- **A specific, non-latest version** (e.g. a runtime an app on the system is pinned to).
Rolling this forward would be wrong — the app needs *that* version. The migration should
preserve the pinned version rather than a roll-forward channel.
## Proposed change
Make `BuildMigrationSelections` (or a dedicated resolver it calls) **network-aware** so it can:
1. Group installed versions by `major.minor.{band}xx` (SDK) / `major.minor.xx` (runtime).
2. Determine whether each installed version is the **latest** within its grouping, using the
signed releases manifest.
- **Latest** ⇒ record the roll-forward band/group channel (intent: track latest; often
controlled by WU/another installer).
- **Not latest** ⇒ treat as a pinned dependency and record the **specific version**, not a
roll-forward channel.
3. Determine whether the version is present in the signed manifest.
- **Present** ⇒ normal (signed) band/version channel.
- **Absent but downloadable from the blob feed** ⇒ record a `-daily` channel (e.g.
`11.0.1xx-daily`) so it remains resolvable, and surface the unsigned-download warning.
# Implementation
This was started but split out of PR #54626 ("Summarize `dotnetup` init
> decisions to simplify UX") to keep that PR focused on the walkthrough summary.
> The unsigned-download warning + blob-feed band fallback that PR #54626 briefly
> added (as a bonus) has been reverted back to the `release/dnup` state; the
> removed code is preserved at the bottom of this document so it can be reused
> here.
>
> **Recovering the implementation:** the revert is a single commit,
> [`02c4f79`](https://github.com/dotnet/sdk/commit/02c4f7971f426db2e70bd9041f07a167ead3cefd)
> ("Revert unsigned-download backstop + blob-feed band fallback to release/dnup",
> from PR #54626 — full SHA `02c4f7971f426db2e70bd9041f07a167ead3cefd`). Most of
> the scaffolding below was already implemented there, so it can be largely
> restored by reverting that commit:
>
> ```bash
> git revert --no-commit 02c4f7971f426db2e70bd9041f07a167ead3cefd # re-applies the removed code
> # or, to inspect exactly what was removed:
> git show 02c4f7971f426db2e70bd9041f07a167ead3cefd
> ```
>
> That gets back `UnsignedDownloadWarning`, the `UnsignedSourcePolicy` backstop
> state, `UpdateChannel.IsVersionScope()`, the `ShouldFallbackToBlobFeed` band/
> prerelease widening, the `InstallExecutor`/`UpdateWorkflow` backstop calls, and
> the synthetic blob-feed test — i.e. the download-side plumbing this issue needs.
> The remaining work (the network-aware migration selector, grouping/"latest"
> detection, and the refactor to share resolution logic) is new.
## Open considerations
- **Performance / UX**: `BuildMigrationSelections` currently runs synchronously in the init
walkthrough display path. Network resolution adds latency. Consider resolving in the
**background** (kick off async, fill in selections as results arrive) and/or caching.
- **Air-gapped scenarios**: a network probe must degrade gracefully offline with a sane
default (today's purely-syntactic mapping is the natural fallback). Do **not** make the
display path hard-depend on the network.
- **Refactor / avoid duplication**: the resolution + blob-feed logic currently lives in
`DotnetArchiveDownloader` (`ResolveManifestEntry`, `ShouldFallbackToBlobFeed`,
`ResolveBlobFeedEntry`) and `ReleaseManifest.TryFindReleaseFile`. Reusing it from the
migration selector means either moving those methods out of the downloader (which drags
along the cache directory + `HttpClient` + per-request args — awkward, and worse for
air-gapped support) or introducing a shared resolution component. Decide on a clean home
for "given a channel/version, what does the manifest say and is it blob-feed-only?" without
duplicating the downloader's logic.
- **No reliable local "daily" marker**: daily vs. official-preview is a *publishing*
distinction, not an intrinsic property of the bits (the official preview *is* a promoted
daily build; same version string, same commit). There is no portable on-disk flag
(MSBuild properties don't discriminate; Authenticode is Windows-only and unreliable). The
authoritative discriminator is **manifest membership**, which is inherently a network check.
### dotnetup version
_No response_
Contributor guide
No contributing guide indexed for this repository
Research direction
Start at MigrationWorkflow.BuildMigrationSelections and VersionToPatchBasedChannel, then inspect DotnetArchiveDownloader's ResolveManifestEntry, ShouldFallbackToBlobFeed, ResolveBlobFeedEntry, and ReleaseManifest.TryFindReleaseFile. Review commit 02c4f7971f426db2e70bd9041f07a167ead3cefd and the synthetic blob-feed test. Done means latest versions retain roll-forward channels, pinned versions retain specificity, daily-only versions resolve through -daily, and offline behavior remains safe.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- cli, developer-experience, networking
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100