dotnet / dotnet/sdk

Network-aware migration channel resolution for `dotnetup`

Open
#54,931 2 comments 0 reactions 0 assignees View on GitHub
Area-dotnetup untriaged
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.