From cd8e4b75032953e86563430a630578f88384ed93 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Thu, 11 Jun 2026 17:14:27 -0700 Subject: [PATCH 01/17] Add `dotnetup` `preview` `release-strategy-plan.md` This is a draft. Resolves https://github.com/dotnet/sdk/issues/54689 --- .../designs/dotnetup-release-strategy.md | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 documentation/general/dotnetup/designs/dotnetup-release-strategy.md diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md new file mode 100644 index 000000000000..c6c322dd0a41 --- /dev/null +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -0,0 +1,165 @@ +# dotnetup `Preview` Release Strategy + +`dotnetup` ships today as a small internal preview built daily from the tip of the internal CI pipeline. SDK `main` consumes that daily build. + +The problem: every daily change reaches every CI consumer, so we cannot let an SDK servicing/release branch depend on a *stable* dotnetup without it being broken by an unrelated daily change once we remove the `.NET Install Script` fallback. + +--- + +## 1. Requirements + +### Decisions: preview / phase 1 + +**Must have** +- Allow others to download prior releases, even when unsupported (the intent is to let customers roll back without our intervention). Prior versions are downloadable today as immutable per-version `ci.dot.net` URLs; the underlying `dotnetbuilds` blob retention window has no formal retention policy for these artifacts, however. +- Easy rollback or re-release when something goes wrong. +- Develop features without risk of them reaching a stable release. +- Preview consumption / telemetry on a dev branch to confirm stability before promoting to stable. +- Only support 'latest' preview to reduce maintenance burden and move fast. + +**Nice to have** +- No PRs required to bump minor versions. +- Automated changelog notes. +- Only maintain one branch at a time. + +**Not necessary** +- Truly supporting multiple dotnetup versions simultaneously. +- Backporting. + +### Decisions: public / stable preview + +**Additional must have** +- CDN routing for high-scale / geolocation downloads (`builds.dotnet.microsoft.com`). + (`ci.dot.net` likely cannot serve a large public download swath.) + +**Additional nice to have** +- `dotnetup` inclusion in a `json` manifest (likely required for automatic updates). + +--- + +## 2. Assumptions + +- `ci.dot.net` could not scale to the full potential dotnetup customer base, even though it is fronted by Azure Front Door over blob storage. +- `stable` needs to scale beyond `ci.dot.net`; `preview` may not (external customers should not build production CI on top of a preview product). +- GitHub Releases may provide scalable / CDN-like downloads (works for dotnet diagnostics). +- `aka.ms` has no Azure Front Door / `x-cache` / Akamai layer of its own. It is a Kestrel app serving a `301` redirect, which is sufficient for `preview` and `daily`. +- `ci.dot.net` per-version build URLs persist long enough for `dotnetup` rollback needs. This is **assumed, not verified.** + +### Verified infrastructure facts + +- The aka.ms daily links are `301` redirects served by **Kestrel** (no CDN of their own): + + `https://aka.ms/dotnet/dotnetup/daily/dotnetup-win-x64.exe` → + + ``` + https://ci.dot.net/public/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe + ``` + + `https://aka.ms/dotnet/dotnetup/daily/dotnetup-win-x64.exe.sha512` → + + + ``` + https://ci.dot.net/public-checksums/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe.sha512 + ``` + +- Both resolved explicit-version URLs return `200` (binary = 16,143,696 bytes; checksum = 128 bytes). +- `ci.dot.net` fronts `dotnetbuilds.blob.core.windows.net` via **Azure Front Door** + (`x-azure-ref` present, `X-Cache: TCP_MISS`, `Cache-Control: public, max-age=604800` = 7 days, + `x-ms-blob-type: BlockBlob`). dotnetup daily already lands here today. +- `builds.dotnet.microsoft.com` fronts `dotnetcli.blob.core.windows.net` via **Akamai** (`Akamai-GRN` present, no `x-azure-ref`). Reaching it requires a promotion (copy) from `dotnetbuilds` → `dotnetcli`, gated by the release team's staging process. The account→CDN mapping is encoded in Arcade's `PublishingConstants.cs`. + +- dotnetup's Arcade channel, with `targetFeeds: DotNetToolsFeeds`) publishes installers/checksums to `dotnetbuilds/public` and + `dotnetbuilds/public-checksums`. i.e. we are **already in staging**. + +- The versioned blob layout `dotnetup/$(DotnetupVersion)/dotnetup-$(TargetRid)$(_DotnetupNativeExt)` + is set in [eng/Publishing.props](../../../../eng/Publishing.props) (`SetDotnetupBlobPaths`). + +- The install scripts default to `Quality=daily` and resolve `https://aka.ms/dotnet/dotnetup/` + ([scripts/get-dotnetup.sh](../../../../scripts/get-dotnetup.sh), + [scripts/get-dotnetup.ps1](../../../../scripts/get-dotnetup.ps1)). + +--- + +## 3. Comparisons + +- **aspire** — owns its own download site (`aspire.dev`) plus npm, WinGet, other packages, and GitHub Releases. Includes tich channel identity (stable / staging / daily / local / pr), release branches, and SHA-derived staging feeds. Promotion is *manual*: the release pipeline is queued against a selected build, extracts its BAR ID, and runs `darc add-build-to-channel`. Product SemVer is hand-authored in `eng/Versions.props`, not auto-bumped. + ([release-process.md](https://github.com/microsoft/aspire/blob/main/docs/release-process.md)) +- **az cli** — `dev` branch is an `edge` build via `aka.ms`. Official uses WinGet, MSI, and ZIP (served from Microsoft-hosted blob storage behind `aka.ms` links) plus Microsoft package feeds. + ([docs](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-windows?pivots=zip)) +- **gh cli** — WinGet, Homebrew, custom Linux package feed. **Stable releases only**; no daily/edge channel. "Build from source" is for contributors, not a distribution channel. + +Takeaway: dotnetup's ideal is the *lighter half* of Aspire's model. i.e. Separate daily from a blessed selector with immutable versioned artifacts. + +--- + +## 4. Versions Policy + +| Channel | Version shape | Meaning | +|----------|---------------|---------| +| daily | `0.x.y` | Moving, as-is, built from active development tip. No stability promise. | +| preview | `0.x.y` | Blessed internal preview. The latest supported version. | +| stable | `1.0.0` | Public, CDN-scaled release (future). | + +Explicit immutable version selectors remain downloadable for historical acquisition and rollback. +These are real, working URLs today — verified `200` on 2026-06-11, e.g.: + +- `https://ci.dot.net/public/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe` +- `https://ci.dot.net/public-checksums/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe.sha512` + + +--- + +## 5. Plan (phased) + +### Phase 0 — Current + +Daily channel available. `aka.ms/dotnet/dotnetup/daily/...` points at the latest published daily +build on `ci.dot.net`. SDK `main` consumes daily. No tags, no GitHub Release, no blessed selector. +The dotnetup CI pipeline ([.vsts-dnup-ci.yml](../../../../.vsts-dnup-ci.yml)) triggers on the +`dnup`, `release/dnup`, and `release/dotnetup` branches today. + +### Phase 1 — Blessed `preview` selector + +1. **Maintain one branch: `release/dnup`.** +2. **Expose both `/daily/` and `/preview/` URLs.** Daily keeps tracking the moving tip; preview resolves to a 'blessed' build. (But still not 'blessed' to actually tell external customers to use in production.) +3. **Change which `/daily/` build `/preview/` points at via an explicit process** + Implement a `release` pipeline that lets an operator select a set of dotnetup release artifacts from a prior daily pipeline run. +4. **The release pipeline:** + - Bumps the patch version using global `msbuild` parameters and determines a preview tag. + - Pushes that tag onto the commit from the selected daily pipeline run. + - Sets `preview` version metadata property as a global property override using the same methodology as the .NET SDK. + - Runs tests on that branch. + - On pass: repins the `/preview/` `aka.ms` url (only if `test` run is not enabled), and creates the GitHub Release with change notes. + +**Recovery — if only a simple revert is needed:** +1. Re-run the `release` pipeline against an existing tag and repoint the `/preview/` URL. + Archival CI URLs exist per version and are confirmed live today, e.g. + `https://ci.dot.net/public/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe` and its + `.../public-checksums/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe.sha512` sidecar. + **Open:** the blob retention/TTL on `dotnetbuilds` is not yet confirmed — if it is finite, + rollback past that window would require re-publishing rather than just repointing. + +**Recovery — fix needed (e.g. security patch on an old branch):** +1. Check out the tagged commit. +2. Create `release/dnup//hotfix-x.y.z` off the tag. +3. Open an internal PR with the fix into that branch and get approval. +4. Run the release pipeline (which runs tests) and repoint the selector. + +This phase allows a public preview. + +In this phase, before public preview we'd remove the fallback to the .NET Install Script, but keep the fallback on the `daily` dotnetup builds, and keep `release/dnup` using the `daily` dotnetup builds to build. (This prevents a broken `dotnetup` from preventing us from shipping a new `dotnetup`.) + +### Phase 2 — Stable on `builds.dotnet.microsoft.com` (tentative) + +Host a `stable` URL on `builds.dotnet.microsoft.com` by coordinating with the release team to use their promotion pipeline — we already push to `dotnetbuilds`, so this is a copy/promote from +`dotnetbuilds` → `dotnetcli` plus storage write permissions granted by the release/dnceng team. + +Similar release process, through coordination. Possibly automate `preview` releases at this point. + +### Phase 3 — Package-manager acquisition (tentative) + +Linux Package Manager Feed / WinGet / Homebrew / etc. Needs discussion with partners. + +### Phase 4 — Inclusion in the .NET SDK (unplanned at this time) + +If dotnetup ever ships inside the SDK, consider shelling it and adding tests to validate it across all in-support SDK versions. From 31b2cf28ab76771eb625275b0f94a83bcdbdf132 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Thu, 11 Jun 2026 17:22:00 -0700 Subject: [PATCH 02/17] Add the very important requirement --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index c6c322dd0a41..75d50ff80a13 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -16,11 +16,13 @@ The problem: every daily change reaches every CI consumer, so we cannot let an S - Develop features without risk of them reaching a stable release. - Preview consumption / telemetry on a dev branch to confirm stability before promoting to stable. - Only support 'latest' preview to reduce maintenance burden and move fast. +- Compliance with MSFT policy. **Nice to have** - No PRs required to bump minor versions. - Automated changelog notes. - Only maintain one branch at a time. +- Minimal manual maintenance effort burdens to release. **Not necessary** - Truly supporting multiple dotnetup versions simultaneously. From 81ccd674e0d5eb1bd862b002fba53ff25287049d Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 15:46:38 -0700 Subject: [PATCH 03/17] fix release plan typo --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 75d50ff80a13..30a1ebb2be27 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -84,7 +84,7 @@ The problem: every daily change reaches every CI consumer, so we cannot let an S ## 3. Comparisons -- **aspire** — owns its own download site (`aspire.dev`) plus npm, WinGet, other packages, and GitHub Releases. Includes tich channel identity (stable / staging / daily / local / pr), release branches, and SHA-derived staging feeds. Promotion is *manual*: the release pipeline is queued against a selected build, extracts its BAR ID, and runs `darc add-build-to-channel`. Product SemVer is hand-authored in `eng/Versions.props`, not auto-bumped. +- **aspire** — owns its own download site (`aspire.dev`) plus npm, WinGet, other packages, and GitHub Releases. Includes rich channel identity (stable / staging / daily / local / pr), release branches, and SHA-derived staging feeds. Promotion is *manual*: the release pipeline is queued against a selected build, extracts its BAR ID, and runs `darc add-build-to-channel`. Product SemVer is hand-authored in `eng/Versions.props`, not auto-bumped. ([release-process.md](https://github.com/microsoft/aspire/blob/main/docs/release-process.md)) - **az cli** — `dev` branch is an `edge` build via `aka.ms`. Official uses WinGet, MSI, and ZIP (served from Microsoft-hosted blob storage behind `aka.ms` links) plus Microsoft package feeds. ([docs](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-windows?pivots=zip)) From 9dd8e51f98384f66eb2d7614740ee963e6be742d Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 15:49:48 -0700 Subject: [PATCH 04/17] Add context that we might not be able to only support latest or that it might not be the right choice for our product/org even if its the convenient / right choice for our team --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 30a1ebb2be27..31625c62c01f 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -153,6 +153,8 @@ In this phase, before public preview we'd remove the fallback to the .NET Instal ### Phase 2 — Stable on `builds.dotnet.microsoft.com` (tentative) +Migrate to `stable` versioning as well. Confirm whether it is actually feasible and correct to only support a latest version if it becomes a 'released' product. + Host a `stable` URL on `builds.dotnet.microsoft.com` by coordinating with the release team to use their promotion pipeline — we already push to `dotnetbuilds`, so this is a copy/promote from `dotnetbuilds` → `dotnetcli` plus storage write permissions granted by the release/dnceng team. From 4db89cfc745d0252d34ddf758996f3fe38a3b96f Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 15:54:44 -0700 Subject: [PATCH 05/17] Consider that the method to bump the patch versions was bogus --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 31625c62c01f..a340ef90067f 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -127,7 +127,7 @@ The dotnetup CI pipeline ([.vsts-dnup-ci.yml](../../../../.vsts-dnup-ci.yml)) tr 3. **Change which `/daily/` build `/preview/` points at via an explicit process** Implement a `release` pipeline that lets an operator select a set of dotnetup release artifacts from a prior daily pipeline run. 4. **The release pipeline:** - - Bumps the patch version using global `msbuild` parameters and determines a preview tag. + - Bumps the patch version by 1 off of the last tagged preview patch version using global `msbuild` parameters. Coordinate to push a PR via maestro or via actions that bumps the version in the real branch. Produce the preview versioned dotnetup `tag`. - Pushes that tag onto the commit from the selected daily pipeline run. - Sets `preview` version metadata property as a global property override using the same methodology as the .NET SDK. - Runs tests on that branch. From 931f3e3297fe9f32b3b1b86cba3587efb7e19169 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 15:57:55 -0700 Subject: [PATCH 06/17] be more specific about the preview property --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index a340ef90067f..6b8e07f8880c 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -129,7 +129,7 @@ The dotnetup CI pipeline ([.vsts-dnup-ci.yml](../../../../.vsts-dnup-ci.yml)) tr 4. **The release pipeline:** - Bumps the patch version by 1 off of the last tagged preview patch version using global `msbuild` parameters. Coordinate to push a PR via maestro or via actions that bumps the version in the real branch. Produce the preview versioned dotnetup `tag`. - Pushes that tag onto the commit from the selected daily pipeline run. - - Sets `preview` version metadata property as a global property override using the same methodology as the .NET SDK. + - Sets `preview` version metadata property `PreReleaseVersionLabel` as a global property override using the same methodology as the .NET SDK. - Runs tests on that branch. - On pass: repins the `/preview/` `aka.ms` url (only if `test` run is not enabled), and creates the GitHub Release with change notes. From 59b67c2d076cb625c7cf0cad12d2c7a775bca528 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 16:01:03 -0700 Subject: [PATCH 07/17] caveat about tests for existing release repoint --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 6b8e07f8880c..710c9c5d28a9 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -134,7 +134,7 @@ The dotnetup CI pipeline ([.vsts-dnup-ci.yml](../../../../.vsts-dnup-ci.yml)) tr - On pass: repins the `/preview/` `aka.ms` url (only if `test` run is not enabled), and creates the GitHub Release with change notes. **Recovery — if only a simple revert is needed:** -1. Re-run the `release` pipeline against an existing tag and repoint the `/preview/` URL. +1. Re-run the `release` pipeline against an existing tag and repoint the `/preview/` URL. Allow skipping tests if needed (break-glass.) Archival CI URLs exist per version and are confirmed live today, e.g. `https://ci.dot.net/public/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe` and its `.../public-checksums/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe.sha512` sidecar. From 40066b21dd82741d27695948e44755720fd9693a Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 16:06:27 -0700 Subject: [PATCH 08/17] Consider arcade rollout timing in plan --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 710c9c5d28a9..76fff4a0b811 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -147,7 +147,7 @@ The dotnetup CI pipeline ([.vsts-dnup-ci.yml](../../../../.vsts-dnup-ci.yml)) tr 3. Open an internal PR with the fix into that branch and get approval. 4. Run the release pipeline (which runs tests) and repoint the selector. -This phase allows a public preview. +This phase allows a public preview. I view arcade rollout as valid around this time once we see stabilization of the public preview, but also potential arcade rollout with a fallback during `preview`. In this phase, before public preview we'd remove the fallback to the .NET Install Script, but keep the fallback on the `daily` dotnetup builds, and keep `release/dnup` using the `daily` dotnetup builds to build. (This prevents a broken `dotnetup` from preventing us from shipping a new `dotnetup`.) From a95b65a5e700874836405cfdfc0a113c18302c3e Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 16:06:43 -0700 Subject: [PATCH 09/17] mention azdo task rollout timeframe --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 76fff4a0b811..2ef566ab8cf9 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -158,7 +158,7 @@ Migrate to `stable` versioning as well. Confirm whether it is actually feasible Host a `stable` URL on `builds.dotnet.microsoft.com` by coordinating with the release team to use their promotion pipeline — we already push to `dotnetbuilds`, so this is a copy/promote from `dotnetbuilds` → `dotnetcli` plus storage write permissions granted by the release/dnceng team. -Similar release process, through coordination. Possibly automate `preview` releases at this point. +Similar release process, through coordination. Possibly automate `preview` releases at this point. Required to rollout to any azdo task. ### Phase 3 — Package-manager acquisition (tentative) From 40cc61eb77a7b229e19805e3b7e44444e31e8b63 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 16:34:46 -0700 Subject: [PATCH 10/17] Add retention investigation and GitHub Release compliance appendices - Appendix A: Empirical HTTP probes show no TTL-based deletion on ci.dot.net/dotnetbuilds; 404s are from promotion-cleanup only. Non-promoted builds (like dotnetup) persist 4+ years. No retention policy documented in arcade or dnceng. - Appendix B: Document 5 teams with automated GitHub Releases (aspire, dotnet-monitor, android-native-tools, cli/cli, mcp). List compliance requirements and recommend dotnet-monitor pattern (gh release create + ManualValidation@1) as simplest for Phase 1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../designs/dotnetup-release-strategy.md | 142 +++++++++++++++++- 1 file changed, 139 insertions(+), 3 deletions(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 2ef566ab8cf9..f98b83e3eb4e 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -45,7 +45,7 @@ The problem: every daily change reaches every CI consumer, so we cannot let an S - `stable` needs to scale beyond `ci.dot.net`; `preview` may not (external customers should not build production CI on top of a preview product). - GitHub Releases may provide scalable / CDN-like downloads (works for dotnet diagnostics). - `aka.ms` has no Azure Front Door / `x-cache` / Akamai layer of its own. It is a Kestrel app serving a `301` redirect, which is sufficient for `preview` and `daily`. -- `ci.dot.net` per-version build URLs persist long enough for `dotnetup` rollback needs. This is **assumed, not verified.** +- `ci.dot.net` per-version build URLs persist long enough for `dotnetup` rollback needs. See [Retention investigation](#retention-investigation-dotnetbuildscinotnet) below. ### Verified infrastructure facts @@ -138,8 +138,11 @@ The dotnetup CI pipeline ([.vsts-dnup-ci.yml](../../../../.vsts-dnup-ci.yml)) tr Archival CI URLs exist per version and are confirmed live today, e.g. `https://ci.dot.net/public/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe` and its `.../public-checksums/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe.sha512` sidecar. - **Open:** the blob retention/TTL on `dotnetbuilds` is not yet confirmed — if it is finite, - rollback past that window would require re-publishing rather than just repointing. + **Open (low risk):** No formal retention/TTL policy is documented for `dotnetbuilds` blobs — see + [Retention investigation](#retention-investigation-dotnetbuildscinotnet). Empirical evidence shows + non-promoted preview builds from 4+ years ago remain accessible, and dotnetup builds are never + subject to promotion-cleanup. We consider the rollback-via-repoint strategy safe for practical + timescales, though confirming definitively requires Azure portal access from dnceng. **Recovery — fix needed (e.g. security patch on an old branch):** 1. Check out the tagged commit. @@ -167,3 +170,136 @@ Linux Package Manager Feed / WinGet / Homebrew / etc. Needs discussion with part ### Phase 4 — Inclusion in the .NET SDK (unplanned at this time) If dotnetup ever ships inside the SDK, consider shelling it and adding tests to validate it across all in-support SDK versions. + +--- + +## Appendix A: Retention Investigation (`dotnetbuilds`/`ci.dot.net`) + +*Investigated 2026-06-12.* + +### Summary + +There is **no documented retention policy** for `dotnetbuilds.blob.core.windows.net` / `ci.dot.net`. +Arcade's `PublishingConstants.cs` `TargetChannelConfig` has no `retentionDays`, `expirationDays`, or +`lifecyclePolicy` field. No retention documentation exists in `dotnet/arcade` or `dotnet/dnceng`. +Any Azure Blob lifecycle policy would be configured in the Azure portal (not in public repos). + +### Empirical HTTP probe results (2026-06-12) + +| Build | Age | `ci.dot.net` | `builds.dotnet.microsoft.com` | Explanation | +|-------|-----|:---:|:---:|---| +| .NET 6.0 preview 1 (Mar 2021) | ~5 yr | ❌ 404 | ✅ 200 | **Promoted** to dotnetcli, cleaned from dotnetbuilds | +| .NET 6.0 preview 7 (Aug 2021) | ~5 yr | ❌ 404 | ✅ 200 | Promoted | +| .NET 7.0 preview 1 (Feb 2022) | ~4.3 yr | ✅ 200 | — | Never promoted; still on staging | +| .NET 7.0 preview 3–7 (Mar–Jul 2022) | ~4 yr | ✅ 200 | — | Never promoted | +| .NET 7.0 RC1/RC2 (Sep–Oct 2022) | ~4 yr | ❌ 404 | ✅ 200 | Promoted | +| .NET 7.0 GA (Nov 2022) | ~4 yr | ❌ 404 | ✅ 200 | Promoted | +| .NET 8.0 preview 1 (Feb 2023) | ~3.3 yr | ✅ 200 | — | Never promoted | +| .NET 9.0 preview 1 (Feb 2024) | ~2.3 yr | ✅ 200 | — | Never promoted | +| dotnetup 0.1.4-preview (Jun 2026) | current | ✅ 200 | — | DotNetToolsFeeds; never promoted | + +### Interpretation + +The 404s on `ci.dot.net` are **not** from age-based TTL deletion. They are the result of a +deliberate **promotion workflow**: once a build graduates from staging (`dotnetbuilds`) to production +(`dotnetcli` / `builds.dotnet.microsoft.com`), the staging copy is cleaned up. + +Since dotnetup uses `DotNetToolsFeeds` (which only targets `dotnetbuilds/public`), its builds are +**never subject to promotion-cleanup**. Non-promoted preview/daily builds from as far back as +Feb 2022 (~4.3 years) remain accessible at this time. + +### Conclusion for dotnetup rollback + +We consider the rollback-via-repoint strategy (pointing `/preview/` at an older versioned URL) safe +for practical timescales. The risk of silent blob deletion is low but cannot be formally ruled out +without Azure portal access from the dnceng team. + +--- + +## Appendix B: Automated GitHub Releases — Compliance & Precedent + +*Investigated 2026-06-12.* + +### Summary + +**Yes — CI pipelines can automatically create GitHub Releases while remaining compliant with +Microsoft policy.** Multiple dotnet/Microsoft teams already do this in production. + +### Teams with automated GitHub Releases + +| Team / Repo | Pattern | Auth mechanism | Human gate | +|---|---|---|---| +| **dotnet/aspire** | AzDO dispatches GitHub Actions via `aspire-repo-bot` App | GitHub App installation token | AzDO pipeline approval stage | +| **dotnet/dotnet-monitor** | AzDO pipeline runs `gh release create` | `dotnet-bot` PAT (KeyVault-backed) | `ManualValidation@1` task | +| **dotnet/android-native-tools** | AzDO `GitHubRelease@1` task | AzDO service connection | `ManualValidation@0` task | +| **cli/cli** | GitHub Actions `workflow_dispatch` | `GITHUB_TOKEN` | `production` environment approval | +| **microsoft/mcp** | AzDO job runs `gh release create` + upload | GitHub App token | Pipeline stage gate | + +### Compliance requirements + +| Requirement | How to meet it | +|---|---| +| Signed binaries | MicroBuild signing (`MicroBuildSignType: Real`) must succeed before release stage | +| SDL / security scanning | 1ES pipeline templates auto-inject APIScan, BinSkim, CodeQL, Component Governance | +| Human accountability | `ManualValidation@1` gate or GitHub environment protection rules | +| Least-privilege auth | GitHub App installation tokens or scoped PATs in KeyVault-backed variable groups | +| Auditability | 1ES `templateContext: type: releaseJob, isProduction: true` on deployment jobs | +| Draft safety net | `isDraft: true` default so accidental gate-pass only creates a draft | + +### Note on Arcade + +Arcade's post-build infrastructure (`post-build.yml`) does **not** include a GitHub Release stage. +GitHub Release creation is a per-repo responsibility — no `GitHubRelease@1` usage exists in +`dotnet/arcade`. Teams implement it themselves using one of the patterns above. + +### Recommendation for dotnetup + +**Recommended: `dotnet-monitor` pattern** (AzDO pipeline + `gh release create` + `ManualValidation@1`). + +Rationale: +- **Simplest** — no GitHub App setup, no cross-system dispatch. Just a pipeline stage that runs + `gh release create` with a PAT from a variable group. +- **Already proven in the dotnet org** — same org, same 1ES template infrastructure, same dnceng team. +- **Draft-by-default** — `--draft` flag means even if the gate is accidentally approved, the release + requires manual publish on GitHub. +- **`ManualValidation@1`** provides the human gate on a `pool: server` job with no agent cost. +- **Scales down** — for `preview` phase we may not even need the manual gate (since preview is + internal), reducing the release to a single pipeline button press. + +The `aspire` pattern (GitHub App → Actions dispatch) is more sophisticated and appropriate if we +later need cross-repo orchestration or complex release-note generation, but adds setup complexity +that isn't justified for Phase 1. + +#### Minimal implementation sketch + +```yaml +# In the dotnetup release pipeline (Phase 1) +- stage: GitHubRelease + displayName: Create GitHub Release + dependsOn: PublishPreview + jobs: + - job: Approval + pool: server + steps: + - task: ManualValidation@1 + inputs: + instructions: 'Approve to create GitHub Release for dotnetup $(ReleaseVersion)' + notifyUsers: '[dotnetup team alias]' + + - job: CreateRelease + dependsOn: Approval + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + - script: | + gh release create "v$(ReleaseVersion)" \ + --repo dotnet/sdk \ + --title "dotnetup $(ReleaseVersion)" \ + --notes-file $(Pipeline.Workspace)/release-notes.md \ + --target $(Build.SourceVersion) \ + --draft + env: + GH_TOKEN: $(BotAccount-dotnet-bot-repo-PAT) + displayName: Create draft GitHub Release +``` From a26d366bf8e74831518c1377a5131f89eed68243 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 16:44:51 -0700 Subject: [PATCH 11/17] compliance requirements should not be ai generated --- .../dotnetup/designs/dotnetup-release-strategy.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index f98b83e3eb4e..5c3b2c782527 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -236,16 +236,6 @@ Microsoft policy.** Multiple dotnet/Microsoft teams already do this in productio | **microsoft/mcp** | AzDO job runs `gh release create` + upload | GitHub App token | Pipeline stage gate | ### Compliance requirements - -| Requirement | How to meet it | -|---|---| -| Signed binaries | MicroBuild signing (`MicroBuildSignType: Real`) must succeed before release stage | -| SDL / security scanning | 1ES pipeline templates auto-inject APIScan, BinSkim, CodeQL, Component Governance | -| Human accountability | `ManualValidation@1` gate or GitHub environment protection rules | -| Least-privilege auth | GitHub App installation tokens or scoped PATs in KeyVault-backed variable groups | -| Auditability | 1ES `templateContext: type: releaseJob, isProduction: true` on deployment jobs | -| Draft safety net | `isDraft: true` default so accidental gate-pass only creates a draft | - ### Note on Arcade Arcade's post-build infrastructure (`post-build.yml`) does **not** include a GitHub Release stage. From 9072e7e641bc73c8f01d6eae344bb1a26524719a Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 16:45:21 -0700 Subject: [PATCH 12/17] remove unneeded section on compliance --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 1 - 1 file changed, 1 deletion(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 5c3b2c782527..f22b18ba6ad0 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -235,7 +235,6 @@ Microsoft policy.** Multiple dotnet/Microsoft teams already do this in productio | **cli/cli** | GitHub Actions `workflow_dispatch` | `GITHUB_TOKEN` | `production` environment approval | | **microsoft/mcp** | AzDO job runs `gh release create` + upload | GitHub App token | Pipeline stage gate | -### Compliance requirements ### Note on Arcade Arcade's post-build infrastructure (`post-build.yml`) does **not** include a GitHub Release stage. From debfe51c2c22df32a2f287014dc14a7de1855b93 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 16:49:48 -0700 Subject: [PATCH 13/17] remove sample yaml this might be helpful for an agent to plan but it is not helpful as documentation --- .../designs/dotnetup-release-strategy.md | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index f22b18ba6ad0..cf38727a8269 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -258,37 +258,3 @@ Rationale: The `aspire` pattern (GitHub App → Actions dispatch) is more sophisticated and appropriate if we later need cross-repo orchestration or complex release-note generation, but adds setup complexity that isn't justified for Phase 1. - -#### Minimal implementation sketch - -```yaml -# In the dotnetup release pipeline (Phase 1) -- stage: GitHubRelease - displayName: Create GitHub Release - dependsOn: PublishPreview - jobs: - - job: Approval - pool: server - steps: - - task: ManualValidation@1 - inputs: - instructions: 'Approve to create GitHub Release for dotnetup $(ReleaseVersion)' - notifyUsers: '[dotnetup team alias]' - - - job: CreateRelease - dependsOn: Approval - pool: - vmImage: ubuntu-latest - steps: - - checkout: self - - script: | - gh release create "v$(ReleaseVersion)" \ - --repo dotnet/sdk \ - --title "dotnetup $(ReleaseVersion)" \ - --notes-file $(Pipeline.Workspace)/release-notes.md \ - --target $(Build.SourceVersion) \ - --draft - env: - GH_TOKEN: $(BotAccount-dotnet-bot-repo-PAT) - displayName: Create draft GitHub Release -``` From f92e512c0ad0e156a249053fcadf6986337d229a Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 12 Jun 2026 16:53:15 -0700 Subject: [PATCH 14/17] add a nice to have that we probably cant have --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index cf38727a8269..47c86a6f1770 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -21,6 +21,7 @@ The problem: every daily change reaches every CI consumer, so we cannot let an S **Nice to have** - No PRs required to bump minor versions. - Automated changelog notes. +- Telemetry informed rollout success gates (this is likely not legally possible) - Only maintain one branch at a time. - Minimal manual maintenance effort burdens to release. From 960b8313284471da7642047eacdd9f89cda00f5d Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Mon, 15 Jun 2026 13:34:39 -0700 Subject: [PATCH 15/17] Remove phase 4 for simplification --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 47c86a6f1770..2e2b0029a9dc 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -21,7 +21,6 @@ The problem: every daily change reaches every CI consumer, so we cannot let an S **Nice to have** - No PRs required to bump minor versions. - Automated changelog notes. -- Telemetry informed rollout success gates (this is likely not legally possible) - Only maintain one branch at a time. - Minimal manual maintenance effort burdens to release. @@ -162,16 +161,12 @@ Migrate to `stable` versioning as well. Confirm whether it is actually feasible Host a `stable` URL on `builds.dotnet.microsoft.com` by coordinating with the release team to use their promotion pipeline — we already push to `dotnetbuilds`, so this is a copy/promote from `dotnetbuilds` → `dotnetcli` plus storage write permissions granted by the release/dnceng team. -Similar release process, through coordination. Possibly automate `preview` releases at this point. Required to rollout to any azdo task. +Similar release process, through coordination. Possibly automate `preview` releases at this point. Required to rollout to any azdo task. ### Phase 3 — Package-manager acquisition (tentative) Linux Package Manager Feed / WinGet / Homebrew / etc. Needs discussion with partners. -### Phase 4 — Inclusion in the .NET SDK (unplanned at this time) - -If dotnetup ever ships inside the SDK, consider shelling it and adding tests to validate it across all in-support SDK versions. - --- ## Appendix A: Retention Investigation (`dotnetbuilds`/`ci.dot.net`) From a6351b595c97e354bb585f677e8c6e6452a69ee2 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Mon, 15 Jun 2026 13:35:18 -0700 Subject: [PATCH 16/17] Simplify github releases appendix --- .../designs/dotnetup-release-strategy.md | 61 +++++++------------ 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index 2e2b0029a9dc..cfdd6ffe6271 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -212,45 +212,30 @@ without Azure portal access from the dnceng team. --- -## Appendix B: Automated GitHub Releases — Compliance & Precedent +## Appendix B: Automated GitHub Releases -*Investigated 2026-06-12.* - -### Summary - -**Yes — CI pipelines can automatically create GitHub Releases while remaining compliant with -Microsoft policy.** Multiple dotnet/Microsoft teams already do this in production. +Automated GitHub Release creation from CI is compliant with Microsoft policy and is established +practice across the dotnet org. Arcade does not provide a shared release stage; each repo +implements its own using one of the patterns below. -### Teams with automated GitHub Releases +### Precedent in the dotnet org -| Team / Repo | Pattern | Auth mechanism | Human gate | +| Repo | Pattern | Auth | Human gate | |---|---|---|---| -| **dotnet/aspire** | AzDO dispatches GitHub Actions via `aspire-repo-bot` App | GitHub App installation token | AzDO pipeline approval stage | -| **dotnet/dotnet-monitor** | AzDO pipeline runs `gh release create` | `dotnet-bot` PAT (KeyVault-backed) | `ManualValidation@1` task | -| **dotnet/android-native-tools** | AzDO `GitHubRelease@1` task | AzDO service connection | `ManualValidation@0` task | -| **cli/cli** | GitHub Actions `workflow_dispatch` | `GITHUB_TOKEN` | `production` environment approval | -| **microsoft/mcp** | AzDO job runs `gh release create` + upload | GitHub App token | Pipeline stage gate | - -### Note on Arcade - -Arcade's post-build infrastructure (`post-build.yml`) does **not** include a GitHub Release stage. -GitHub Release creation is a per-repo responsibility — no `GitHubRelease@1` usage exists in -`dotnet/arcade`. Teams implement it themselves using one of the patterns above. - -### Recommendation for dotnetup - -**Recommended: `dotnet-monitor` pattern** (AzDO pipeline + `gh release create` + `ManualValidation@1`). - -Rationale: -- **Simplest** — no GitHub App setup, no cross-system dispatch. Just a pipeline stage that runs - `gh release create` with a PAT from a variable group. -- **Already proven in the dotnet org** — same org, same 1ES template infrastructure, same dnceng team. -- **Draft-by-default** — `--draft` flag means even if the gate is accidentally approved, the release - requires manual publish on GitHub. -- **`ManualValidation@1`** provides the human gate on a `pool: server` job with no agent cost. -- **Scales down** — for `preview` phase we may not even need the manual gate (since preview is - internal), reducing the release to a single pipeline button press. - -The `aspire` pattern (GitHub App → Actions dispatch) is more sophisticated and appropriate if we -later need cross-repo orchestration or complex release-note generation, but adds setup complexity -that isn't justified for Phase 1. +| dotnet/aspire | AzDO dispatches GitHub Actions via `aspire-repo-bot` App | GitHub App token | AzDO approval stage | +| dotnet/dotnet-monitor | AzDO pipeline runs `gh release create` | KeyVault-backed `dotnet-bot` PAT | `ManualValidation@1` | +| dotnet/android-native-tools | AzDO `GitHubRelease@1` task | AzDO service connection | `ManualValidation@0` | +| cli/cli | GitHub Actions `workflow_dispatch` | `GITHUB_TOKEN` | `production` environment approval | +| microsoft/mcp | AzDO job runs `gh release create` + upload | GitHub App token | Pipeline stage gate | + +### dotnetup approach + +dotnetup uses the `dotnet-monitor` pattern: an AzDO pipeline stage runs `gh release create --draft` +with a KeyVault-backed PAT, gated by `ManualValidation@1`. + +- No GitHub App or cross-system dispatch to set up — a single pipeline stage on existing 1ES/dnceng + infrastructure. +- `--draft` ensures a release is never published without a manual confirmation on GitHub. +- `ManualValidation@1` runs on a `pool: server` job with no agent cost. +- For `preview`, the manual gate may be dropped since the channel is internal, reducing release to a + single pipeline run. From a2ed00acbae56d4e20cf60c79164303157ee23c7 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Mon, 15 Jun 2026 13:37:29 -0700 Subject: [PATCH 17/17] address copilot nits --- .../general/dotnetup/designs/dotnetup-release-strategy.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md index cfdd6ffe6271..ac38cff836a3 100644 --- a/documentation/general/dotnetup/designs/dotnetup-release-strategy.md +++ b/documentation/general/dotnetup/designs/dotnetup-release-strategy.md @@ -1,6 +1,6 @@ # dotnetup `Preview` Release Strategy -`dotnetup` ships today as a small internal preview built daily from the tip of the internal CI pipeline. SDK `main` consumes that daily build. +`dotnetup` ships today as a small internal preview built daily from the tip of the internal CI pipeline. SDK `main` consumes that daily build. The problem: every daily change reaches every CI consumer, so we cannot let an SDK servicing/release branch depend on a *stable* dotnetup without it being broken by an unrelated daily change once we remove the `.NET Install Script` fallback. @@ -45,7 +45,7 @@ The problem: every daily change reaches every CI consumer, so we cannot let an S - `stable` needs to scale beyond `ci.dot.net`; `preview` may not (external customers should not build production CI on top of a preview product). - GitHub Releases may provide scalable / CDN-like downloads (works for dotnet diagnostics). - `aka.ms` has no Azure Front Door / `x-cache` / Akamai layer of its own. It is a Kestrel app serving a `301` redirect, which is sufficient for `preview` and `daily`. -- `ci.dot.net` per-version build URLs persist long enough for `dotnetup` rollback needs. See [Retention investigation](#retention-investigation-dotnetbuildscinotnet) below. +- `ci.dot.net` per-version build URLs persist long enough for `dotnetup` rollback needs. See [Retention investigation](#appendix-a-retention-investigation-dotnetbuildscidotnet) below. ### Verified infrastructure facts @@ -70,7 +70,7 @@ The problem: every daily change reaches every CI consumer, so we cannot let an S `x-ms-blob-type: BlockBlob`). dotnetup daily already lands here today. - `builds.dotnet.microsoft.com` fronts `dotnetcli.blob.core.windows.net` via **Akamai** (`Akamai-GRN` present, no `x-azure-ref`). Reaching it requires a promotion (copy) from `dotnetbuilds` → `dotnetcli`, gated by the release team's staging process. The account→CDN mapping is encoded in Arcade's `PublishingConstants.cs`. -- dotnetup's Arcade channel, with `targetFeeds: DotNetToolsFeeds`) publishes installers/checksums to `dotnetbuilds/public` and +- dotnetup's Arcade channel, with `targetFeeds: DotNetToolsFeeds` publishes installers/checksums to `dotnetbuilds/public` and `dotnetbuilds/public-checksums`. i.e. we are **already in staging**. - The versioned blob layout `dotnetup/$(DotnetupVersion)/dotnetup-$(TargetRid)$(_DotnetupNativeExt)` @@ -139,7 +139,7 @@ The dotnetup CI pipeline ([.vsts-dnup-ci.yml](../../../../.vsts-dnup-ci.yml)) tr `https://ci.dot.net/public/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe` and its `.../public-checksums/dotnetup/0.1.4-preview.4.26303.1/dotnetup-win-x64.exe.sha512` sidecar. **Open (low risk):** No formal retention/TTL policy is documented for `dotnetbuilds` blobs — see - [Retention investigation](#retention-investigation-dotnetbuildscinotnet). Empirical evidence shows + [Retention investigation](#appendix-a-retention-investigation-dotnetbuildscidotnet). Empirical evidence shows non-promoted preview builds from 4+ years ago remain accessible, and dotnetup builds are never subject to promotion-cleanup. We consider the rollback-via-repoint strategy safe for practical timescales, though confirming definitively requires Azure portal access from dnceng.