- name
- release-readiness
- description
- Assesses ship-readiness for .NET MAUI release branches — Servicing Releases (SR) and Previews — and produces public-safe, copy-ready release handoffs or Loop-page drafts from the resulting evidence. Use for readiness verdicts, release blockers, Preview/SR status, release handoff pages, manual validation instructions, or "make the SR10/Preview N release page." Surveys CI and release delta, classifies regressions, and keeps Preview and servicing semantics distinct.
- metadata
- {"author":"dotnet-maui","version":"2.0"}
- compatibility
- Requires `gh` CLI authenticated with `repo` + `read:org` scopes. Local net11 enrichment also uses `az` CLI with access to dnceng/internal; it fails open when unavailable and is always skipped in GitHub Actions. Preview installability uses NuGet v3 feeds; an optional short-lived Azure DevOps PAT with Packaging Read scope may be needed for an authenticated shipping feed. Run from a checkout of `dotnet/maui`.
# Release Readiness
This skill produces deterministic, evidence-backed answers to **"Is `<release branch>` ready to ship?"** for .NET MAUI release branches — both **Servicing Releases (SR)** and **Previews**, in both **in-flight** and **candidate** (pre-cut) modes.
## 🚨 Report-only
This skill **reports**. It does **not** execute release operations against dotnet/maui — no branch cuts, no SR merges, no tags, no pushes to `release/*` refs. If you (the agent/user invoking this skill) are asked to perform a release operation, refuse and emit the recommended commands as a copy-pasteable block for the human release captain to run.
## When to Use
- "How does SR8 look?" / "Is SR8 ready to ship?"
- "What's blocking SR9 candidate?" / "What would ship if we cut SR9 today?"
- "How does net11 preview6 look?" / "Are we ready to cut preview6 from net11.0?"
- "Are there any regression fixes I should backport to SR8?"
- "What's new in SR8 since the last sync?"
- "Give me a status on all releases" / "release status overview" / "what needs attention across releases" (**portfolio** — read the open `[Release Readiness]` tracker issues first; see [Reading trackers directly](#reading-trackers-directly-ad-hoc-status) below)
- Scheduled and event-driven release tracking across all active majors
> **For per-PR regression risk** (deletions reverting prior bug-fix lines), use [`find-regression-risk`](../find-regression-risk/SKILL.md) instead — it answers a different question.
## Architecture
### Branch roles: configured train versus historical releases
Read `eng/release-rollover.json` through
`eng/scripts/Get-ReleaseRolloverPlan.ps1 -ResolveStateOnly`. Configuration is
maintained once; callers do not supply cycle numbers at runtime. The readiness
helper `scripts/ReleaseBranchRoles.ps1` reuses that validator and the shared
`Get-MainBranchForVersion` resolver, rather than deriving ownership from the
largest branch name or feature-main's version.
For the configured `11.0.1xx` / SR1 train:
| Role | Ref |
|---|---|
| Initial fix review, release integration and cut source | literal `net11` (not historical `net11.0`) |
| Next-major features | `main` (.NET 12); daily flow is `net11 → main`, never features back into `net11` |
| Selected current content | `candidate/11.0.1xx-sr1` |
| Future content | `inflight/11.0.1xx-sr2` |
| Release after candidate integration | `release/11.0.1xx-sr1`, cut from approved `net11` |
| Active nightly | resolver's `nightlyBranch`: `inflight/current` in legacy mode, versioned inflight in versioned mode |
Fix PRs initially target `net11` for review. After approval, a maintainer manually
retargets each PR to the active versioned inflight branch and merges it directly
there. Bots must not retarget or merge. Required CI validates the final base.
An approved fix on inflight but not yet on `net11` is normal staged content:
promotion through candidate integration, not a direct individual-fix merge,
brings selected contents into `net11`.
The selected `candidateNamingPolicy=candidate` convention prepares candidate SR
N+1 and future inflight SR N+2 after a verified candidate merge. Older
`unresolved` configurations still block mutation, not read-only reporting.
The deployed merged-candidate workflow stages the exact CI candidate merge and
proposes reviewed next-cycle CI on net11. Independently, publication opens a
separate draft servicing/stable PR targeting `automation/release/<band>-srN`
before the real release exists. The candidate itself includes its upcoming
bug-template version and never carries the servicing flip. Signing waits for
that separate human-reviewed two-parent servicing merge, followed by a manual
real release cut at that exact merge and matching internal mirror. No post-cut
servicing PR is opened. Receipts do not change canonical
`currentSr`/`nightlyMode`. When the approved
release-state update advances to SR2, readiness selects
`candidate/11.0.1xx-sr2` and versioned nightly `inflight/11.0.1xx-sr3`
automatically. It never writes state.
Resolved roles are configuration evidence only: branch existence does not prove
deployed branch protections, correct required-check wiring, or a signed build.
Report those deployment/validation gaps independently; never enable mutations
because a branch happens to exist.
The configured SR candidate inventories **the selected staged ref**, not the
latest fixes or feature branch. Source-fix ancestry, open source PRs, and
next-cycle version checks use the resolved fixes branch. JSON compatibility
names (`mainBranch`, `onMain`, `merged-on-main-no-backport`, `open-on-main`) mean
that resolved source, which is also emitted explicitly as `sourceBranch`.
Candidate evidence is pre-flight, never proof of a release cut or signed build.
The report verifies both ancestry directions. If the fixes source contains
commits absent from the candidate snapshot, `candidateIntegration` records the
relationship/count and a prominent synchronization WATCH explicitly states that
the candidate-only inventory is **not the full prospective cut**. Inventory the
approved integrated source commit separately, or synchronize the staged snapshot,
before certifying release contents. No synthetic union is assumed.
**Configured servicing progress is tag-independent.** Maintainers create tags
manually when releasing; automation does not create/move tags, infer shipping
from them, or require them for cuts, normalization, next lanes or signing
preparation. SR1 inventories against `release/<band>` (the first GA branch);
later SRs use the preceding `release/<band>-srN`. Both the content source and
baseline are pinned to full commit SHAs at report time. The configured current
cycle and its preceding cut remain visible independently of tags.
If a preceding release branch is absent, emit an incomplete **UNKNOWN / NOT
RUN** inventory diagnostic, not zero findings and not a missing-tag blocker.
Remove stale inventory exports when reusing an output directory. Wrong source
versions and invalid explicit revisions still fail. Human closure is honored
for each exact source/baseline snapshot; a new snapshot can produce a new
generation without tags or a false shipped claim.
Historical majors remain independent: `.NET 10`, Preview/RC branches, explicit
source/survey overrides, immutable shipped-tag bounds, tag retirement and
Release Captain Notes retain their own lifecycle. Historical examples below
using `main` or `net11.0` describe those releases, not active .NET 11 ownership.
This skill has **four** PowerShell entry points, one Preview helper, and one workflow:
| Script | Branch type | Purpose |
|--------|-------------|---------|
| [`Find-ReleaseReadinessTrackers.ps1`](scripts/Find-ReleaseReadinessTrackers.ps1) | all | Uses configured cycle state and pinned branches for servicing progress; historical/unconfigured and Preview/RC trackers retain their tag rules. Emits the tracker JSON consumed by the workflow. |
| [`Get-ReleaseReadiness.ps1`](scripts/Get-ReleaseReadiness.ps1) | SR | Full readiness report for a single SR branch (in-flight, `-Candidate`, or `-Shipped`). `-Shipped` surveys the same branch with post-ship verdict, carry-forward, and hotfix-vs-next-SR guidance semantics. |
| [`Get-PreviewReadiness.ps1`](scripts/Get-PreviewReadiness.ps1) | Preview / RC | Full readiness report for a single prerelease branch (in-flight or candidate via `-Mode candidate -SurveyRef net<major>.0`). Preview reports include consumer-installability evidence; RC reports retain that check as `UNKNOWN` until RC workload-set package resolution is supported. |
| [`PreviewInstallability.ps1`](scripts/PreviewInstallability.ps1) | Preview helper | Resolves the workload-set package, validates branch-pin coherence, probes manifest and representative pack availability, extracts platform prerequisites, and emits an isolated NuGet configuration for local validation. |
| [`New-ReleaseHandoff.ps1`](scripts/New-ReleaseHandoff.ps1) | all | Projects existing readiness JSON plus separately verified public release evidence into copy-ready Markdown and normalized JSON. Missing facts remain `TBD`; it never selects builds or mutates release state. |
| [`release-readiness.yml`](../../workflows/release-readiness.yml) | both | Three-hourly daytime UTC schedule + event-driven refreshes + manual dispatch + PR validation. Non-PR triggers run `Find-Trackers -AllActiveMajors`, fan out a matrix job per tracker, and write idempotent `[Release Readiness]` issues; PR triggers validate outputs only. |
Shared support code lives in [`PublicReportSanitizer.ps1`](scripts/PublicReportSanitizer.ps1) for public Markdown/JSON redaction and [`TrackerIssueLifecycle.sh`](scripts/TrackerIssueLifecycle.sh) for tested issue-selection and race-compensation primitives.
### Historical tag-existence rule
The rules below apply to historical/unconfigured trains and Preview/RC
reporting. They do **not** control configured GA/SR progress, which uses branch
snapshots and canonical cycle state. Automatic milestone processing also ignores
stable tag-push events for that configured train; manual tag diagnostics remain
available separately.
The trackers detector is grounded in **tag existence as the source of truth for "shipped vs in-flight"**. A release is in-flight if and only if its expected tag has NOT been published — branch existence, commit recency, and milestone state are all secondary signals.
- SR shipped tag pattern: `<major>.0.<patch>` (e.g. `10.0.71` shipped → SR7 retired, no longer produces a tracker)
- Preview shipped tag pattern: `<major>.0.0-preview.<N>.<date>[.<build>]` (e.g. `11.0.0-preview.5.26304.4` shipped → preview5 no longer produces a tracker)
- RC shipped tag pattern: `<major>.0.0-rc.<N>.<date>[.<build>]` (e.g. `11.0.0-rc.1.26425.128` shipped → RC1 no longer produces a tracker)
Preview/RC retirement also recognizes the published SDK-band `.0.100-...`
tags (for example `11.0.100-preview.7.26410.2` and
`11.0.100-rc.1.26458.5`). Retained old prerelease branches are not revived as
in-flight merely because their immutable tags use that package-version format.
**Post-ship lifecycle (`shipped` mode).** Most shipped SRs are retired the moment their tag exists. The **one exception** is the *most-recently-shipped* SR (highest shipped patch), which keeps emitting as `mode='shipped'` so its tracker issue stays useful through post-ship follow-up — adding the new build to the GitHub issue version dropdown, publishing release notes, closing out the milestone. Shipped reports never retroactively return `Not Ready`: unresolved work is split into urgent hotfix-vs-next-SR follow-ups and structured carry-forward items. Each immutable shipped tag is **create-once**: if it was never tracked (for example, the tag appeared before a scheduled updater ran), the workflow creates it; once a human closes that exact tagged generation, scheduled runs do not resurrect it. An untagged hotfix is explicitly marked `hotfixInProgress=true`, forces a yellow follow-up verdict, and is likewise create-once. Hotfix closure is scoped to the live version **and branch commit**: the same generation stays closed, while new commits or a new version can create fresh evidence. Workflow decisions use the generated report markers, not detector-time hotfix fields, so tag/commit changes between detection and reporting cannot apply stale lifecycle state. Older shipped SRs remain retired.
Shipped commit inventory and fix ancestry are evaluated against the immutable stable tag and the prior SR cycle's latest stable tag, never against the mutable SR branch or current `main`. This keeps the full SR inventory visible when an SR publishes multiple hotfix tags. If either tag does not resolve locally, generation fails with an explicit fetch instruction rather than emitting a falsely empty/clean tracker. During the normal tag-before-GitHub-Release window, the local stable tag is already authoritative for immutable contents and the report labels its date as tagged-commit evidence until publication metadata appears. If the Releases API is unavailable, the same immutable local-tag bounds remain usable, but the report emits a publication-status-unknown warning instead of claiming the tag is published.
Both report generators dot-source [`PublicReportSanitizer.ps1`](scripts/PublicReportSanitizer.ps1), so public Markdown and JSON use one shared redaction implementation.
If the live SR branch has commits after the latest stable tag or has already bumped toward an untagged hotfix in the same SR patch decade, the detector keeps the latest shipped SR in `shipped` mode, anchors contents to the stable tag, and emits a WATCH follow-up. This catches the pre-bump window as soon as branch HEAD advances rather than waiting for `PatchVersion` to change.
## Quick Start
### One-shot portfolio report (matches a full scheduled fan-out)
```bash
# Detect every active in-flight + candidate tracker across all active majors
pwsh .github/skills/release-readiness/scripts/Find-ReleaseReadinessTrackers.ps1 \
-AllActiveMajors \
-OutputJson trackers.json
# Emits a JSON envelope with one tracker per active branch, each carrying:
# branchType: 'sr' | 'preview' | 'rc'
# branchName: canonical proposed branch slug (always populated)
# branchExists: true if the branch is on origin, false for candidates
# mode: 'in-flight' | 'candidate' | 'shipped'
# hotfixInProgress: true only when the latest shipped SR branch is ahead of its stable tag
# hotfixVersion/hotfixCommit: mutable hotfix generation used for close/recreate idempotency
# surveyRef: ref to actually survey (branch itself, or net<major>.0 for candidates)
# canonicalKey: stable join key (e.g. net10-sr8, net11-preview6, net11-rc1)
# issueTitle: title for the maintained tracker issue
# regressionLabels: list of regressed-in-* labels relevant to this branch
```
### SR (Servicing Release)
```bash
# Configured current candidate: automatic staged ref + fixes source, no SR0.
# Uses the pinned GA/prior release branch; no tag is required.
pwsh .github/skills/release-readiness/scripts/Get-ReleaseReadiness.ps1 \
-SrBranch release/11.0.1xx-sr1 -Candidate \
-RegressionLabels regressed-in-11.0.0,regressed-in-11.0.10 \
-TrackerKey net11-sr1 \
-OutputDir CustomAgentLogsTmp/release-readiness/sr1-candidate
# In-flight SR
pwsh .github/skills/release-readiness/scripts/Get-ReleaseReadiness.ps1 \
-SrBranch release/10.0.1xx-sr8 \
-RegressionLabels regressed-in-10.0.70,regressed-in-10.0.80 \
-TrackerKey net10-sr8 \
-OutputDir CustomAgentLogsTmp/release-readiness/sr8
# SR candidate (no branch yet — survey main; pass the PRIOR SR as -SrBranch)
pwsh .github/skills/release-readiness/scripts/Get-ReleaseReadiness.ps1 \
-SrBranch release/10.0.1xx-sr8 \
-Candidate \
-RegressionLabels regressed-in-10.0.80,regressed-in-10.0.90 \
-TrackerKey net10-sr9 \
-OutputDir CustomAgentLogsTmp/release-readiness/sr9-candidate
```
### Preview
```bash
# In-flight preview
pwsh .github/skills/release-readiness/scripts/Get-PreviewReadiness.ps1 \
-Branch release/11.0.1xx-preview6 \
-Mode in-flight \
'-PublicSafe:$false' \
-TrackerKey net11-preview6 \
-OutputDir CustomAgentLogsTmp/release-readiness/preview6
# Preview candidate (branch not cut yet — survey net11.0 instead)
pwsh .github/skills/release-readiness/scripts/Get-PreviewReadiness.ps1 \
-Branch release/11.0.1xx-preview6 \
-Mode candidate \
-SurveyRef net11.0 \
'-PublicSafe:$false' \
-TrackerKey net11-preview6 \
-OutputDir CustomAgentLogsTmp/release-readiness/preview6-candidate
```
### Release Candidate
```bash
pwsh .github/skills/release-readiness/scripts/Get-PreviewReadiness.ps1 \
-Branch release/11.0.1xx-rc1 \
-Mode in-flight \
'-PublicSafe:$false' \
-TrackerKey net11-rc1 \
-OutputDir CustomAgentLogsTmp/release-readiness/rc1
```
The unattended public survey does not know the release-owner-confirmed workload-set
version or private shipping source. It therefore keeps **Consumer installability**
`UNKNOWN` rather than guessing that the newest coherent package is the blessed one.
Complete the local gate below before declaring a Preview ready.
### Copy-ready release handoff / Loop draft
Generate the appropriate Preview or SR readiness JSON first. Then follow
[`references/release-handoff.md`](references/release-handoff.md) to gather
separately verified public build, test, assessment, rollback, and workload-set
evidence and render it:
```bash
pwsh .github/skills/release-readiness/scripts/New-ReleaseHandoff.ps1 \
-ReadinessJson ./release-readiness.json \
-EvidenceJson ./release-evidence.json \
-OutputDir ./release-handoff
```
This is a deterministic projection of the readiness report, not a second survey.
It supports Preview and SR (including SR10) through one editorial renderer while
preserving their different readiness semantics. It does not read or write Loop
or SharePoint. Do not copy private source-page content into the evidence file;
unknown fields must remain `TBD`.
### Preview: local net11 official-build health
For net11 preview runs through this skill from a local checkout, invoke
`Get-PreviewReadiness.ps1 '-PublicSafe:$false'`. The script then automatically
queries the internal official `dotnet-maui` pipeline (Azure DevOps definition
`1095`, org `dnceng`, project `internal`) when the current Azure CLI identity has
access. No build ID is required. It independently checks:
1. The resolved active nightly ref for the configured .NET11 train; for an
unconfigured historical train, its per-major source ref.
2. `refs/heads/release/11.0.1xx-previewN` — the evaluated release branch, when
that branch exists.
Candidate mode still checks that source/nightly lane; it adds the prospective release ref only
after that ref exists. Identical refs are queried once. The local report includes
each branch's health classification, build ID and number, pipeline status/result,
source SHA, and internal build URL. A failed or canceled current build is `red`;
a partially successful build is `partial-success`; a build behind the newest
trigger-eligible commit is `stale`; a queued/running build is `in-progress`;
missing or malformed evidence is `unknown`.
Discovery examines a bounded five-build window. It prefers a build at exact branch
HEAD, then scans by queue time and skips only candidates proven stale before
accepting one proven current. Indeterminate candidates are buffered: disagreeing
possible outcomes remain `unknown`, while a later proven-current failure remains
`red` only when every buffered candidate is also a completed failure/cancellation.
A terminal window containing only same-branch failed/canceled indeterminate builds
also remains blocking as `failed-or-stale` because every candidate is either red
or stale. Its rendering preserves that uncertainty and requires restoring currency
evidence before choosing failure repair versus a current-HEAD rerun. This prevents
both false readiness upgrades and loss of certain blocking evidence.
The internal check is intentionally fail-open:
- `GITHUB_ACTIONS=true` skips it before any Azure command runs.
- Missing Azure CLI, expired login, or inaccessible dnceng/internal access yields
`skipped` and does not downgrade the public-data verdict.
- Azure CLI and GitHub branch queries have bounded execution; a timeout yields
`unknown` rather than hanging the local readiness run.
- Local `red`/`stale`/`failed-or-stale` maps to `BLOCKED`,
`in-progress`/`partial-success` to `WATCH`, and `unknown` to `UNKNOWN`.
For `failed-or-stale`, restore build-currency evidence first; then either repair
the failed build if it is current or run the official build at current HEAD if
it is stale.
- `-PublicSafe:$true` omits all internal IDs, SHAs, URLs, and branch rows. The
public workflow uses this behavior and never receives internal credentials.
The script remains public-safe by default. This skill and the release-readiness
agent explicitly pass `'-PublicSafe:$false'` for enriched local net11 reports;
never reuse those artifacts in a public tracker issue. `-IncludeInternal`
remains an explicit compatibility override when a caller requests a sanitized
internal classification, and `-InternalBuildId` remains a diagnostic override
for the evaluated release branch.
### Preview: authoritative blessed-build source (.NET Release Tracker)
For **Previews**, this skill's public survey (CI health + regression classification on `net<major>.0` or the preview branch) tells you whether the code is *ready*, but it **cannot on its own name which staged build is the official, blessed preview** — that designation lives in the private **.NET Release Tracker** plugin. So when answering *"run release readiness … is net11 preview6 ready?"* / *"which build is the official preview6?"*, consult that authoritative source **in addition to** running `Get-PreviewReadiness.ps1`:
1. **Classify access first (deterministic gate — fetches no release data, always exits 0):**
```bash
pwsh ./.github/skills/dependency-flow/scripts/Get-PreviewReleaseReadiness.ps1
# -> RELEASE_TRACKER_STATUS=NO_ACCESS | ACCESS_ON_INACTIVE_ACCOUNT | AVAILABLE_NOT_ENABLED | AVAILABLE_ENABLED
```
GitHub에서 보기