Skip to content

Add preview/RC milestone support with release branch detection#34999

Merged
PureWeen merged 11 commits into
mainfrom
feature/milestone-preview-support
Apr 16, 2026
Merged

Add preview/RC milestone support with release branch detection#34999
PureWeen merged 11 commits into
mainfrom
feature/milestone-preview-support

Conversation

@PureWeen
Copy link
Copy Markdown
Member

@PureWeen PureWeen commented Apr 16, 2026

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Summary

Follow-up to #34686. Adds preview/RC milestone support and release branch detection to the milestone drift fixer.

Problem

PRs merged to `net11.0` were not being milestoned because `Versions.props` on that branch always has `PreReleaseVersionIteration=1` regardless of which preview the PR actually ships in. The iteration is only bumped on release branches.

Solution

Release Branch Detection (Primary)

New detection step checks release branches first using `merge-base --is-ancestor`. For each PR, it finds the earliest release branch containing the merge commit:

Release Branch Milestone
`release/10.0.1xx` `.NET 10.0 GA`
`release/10.0.1xx-sr5` `.NET 10 SR5`
`release/11.0.1xx-preview1` `.NET 11.0-preview1`
`release/11.0.1xx-preview3` `.NET 11.0-preview3`
`release/12.0.1xx-rc1` `.NET 12.0-rc1`

Detection Order

  1. Explicit `-Tag` — if provided
  2. Release branches — `merge-base --is-ancestor` against `release/{Major}.0.1xx-*` branches, earliest match wins
  3. Versions.props at merge commit — fallback for PRs not yet on any release branch
  4. Tag range search — last resort

Preview/RC Milestone Mapping

`ConvertTo-Milestone` now accepts optional pre-release label and iteration:

Input Milestone
`11.0.0 + preview + 3` `.NET 11.0-preview3`
`12.0.0 + rc + 1` `.NET 12.0-rc1`
`10.0.60` (stable) `.NET 10 SR6` (unchanged)

Validated Results

PR Base Current Milestone Script Result Method
#33524 net11.0 .NET 11.0-preview1 .NET 11.0-preview1 ✅ Release branch
#33233 net11.0 .NET 11.0-preview1 .NET 11.0-preview1 ✅ Release branch
#30132 net11.0 .NET 11.0-preview3 .NET 11.0-preview3 ✅ Release branch
#33834 net11.0 .NET 11.0-preview3 .NET 11.0-preview3 ✅ Release branch
#34214 net11.0 .NET 11.0-preview2 .NET 11.0-preview3 ✅ Release branch (drift caught!)
#34945 net11.0 .NET 11.0-preview4 preview1 (fallback) Versions.props (no p4 branch yet)
#34620 main .NET 10 SR6 .NET 10 SR6 ✅ Release branch
#34047 main .NET 10 SR4.1 .NET 10 SR5 ✅ Release branch (drift caught!)

PR #34214 is a real drift example: milestoned preview2 by a human, but actually on the preview3 release branch.

Test Suite

88 Pester tests (11 new):

  • 6 for `ConvertTo-Milestone` preview/RC mapping
  • 5 for `ConvertBranchToMilestone` (GA, SR, preview, RC, non-release)

Get-VersionFromGitRef now reads PreReleaseVersionLabel and
PreReleaseVersionIteration from eng/Versions.props. ConvertTo-Milestone
maps these to preview/RC milestone names:

  11.0.0 + preview + 3  →  .NET 11.0-preview3
  12.0.0 + rc + 1       →  .NET 12.0-rc1
  10.0.60 (stable)      →  .NET 10 SR6 (unchanged)

Stable labels (ci.main, ci.inflight, servicing) are treated as
non-prerelease and continue to use the GA/SR mapping.

6 new tests for preview/RC mapping (83 total, all passing).
Backward compatible — .NET 10 stable PRs and tag mode unaffected.

Validated:
- PR #34252 (net11.0, preview1) → .NET 11.0-preview1 ✅
- PR #34620 (.NET 10 stable) → .NET 10 SR6 ✅
- Tag 10.0.50 → 78 PRs, 90 corrections ✅

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 16, 2026 13:50
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 16, 2026

🚀 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/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34999

Or

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the milestone drift fixer script to support preview/RC milestone naming based on additional version metadata in eng/Versions.props, enabling correct milestoning for PRs merged during pre-release phases (e.g., net11.0 previews).

Changes:

  • Update Get-VersionFromGitRef to also read PreReleaseVersionLabel and PreReleaseVersionIteration from eng/Versions.props.
  • Extend ConvertTo-Milestone to generate .NET X.0-previewN / .NET X.0-rcN milestone names when appropriate.
  • Add Pester tests covering preview/RC milestone mapping cases.

Reviewed changes

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

File Description
.github/scripts/Fix-MilestoneDrift.ps1 Adds prerelease metadata detection and preview/RC milestone mapping logic.
.github/scripts/Fix-MilestoneDrift.Tests.ps1 Adds unit tests validating preview/RC milestone name generation.

Comment thread .github/scripts/Fix-MilestoneDrift.ps1 Outdated
Comment on lines +515 to +517
$preLabel = if ($script:_preLabel) { $script:_preLabel } else { $null }
$preIter = if ($script:_preIter) { $script:_preIter } else { 0 }
$expectedMs = ConvertTo-Milestone $ReleaseTag $preLabel $preIter
Comment on lines +134 to +139
if ($joined -match '<PreReleaseVersionLabel[^>]*>([^<]+)</PreReleaseVersionLabel>') {
$rawLabel = $Matches[1]
# Only treat "preview" and "rc" as pre-release; "ci.main", "ci.inflight", "servicing" are stable builds
if ($rawLabel -match '^(preview|rc)$') {
$preLabel = $rawLabel
if ($joined -match '<PreReleaseVersionIteration>(\d+)</PreReleaseVersionIteration>') {
Comment thread .github/scripts/Fix-MilestoneDrift.ps1 Outdated
Comment on lines +103 to +104
PreReleaseLabel (e.g. "preview", "rc", or $null for stable),
and PreReleaseIteration (e.g. 3).
Instead of relying solely on Versions.props (which has stale
PreReleaseVersionIteration on net11.0), check release branches
first using merge-base --is-ancestor.

Detection order:
1. Explicit -Tag (if provided)
2. Release branches: find earliest release/X.0.1xx-* branch
   containing the merge commit (GA first, then SR/preview/RC)
3. Versions.props at merge commit (fallback for unreleased PRs)
4. Tag range search (last resort)

New functions:
- ConvertBranchToMilestone: maps branch names to milestone names
  (release/11.0.1xx-preview3 → .NET 11.0-preview3)
- Find-ReleaseBranchForCommit: checks ancestry against all release
  branches for a major version, returns earliest match

Validated:
- Preview 3 PRs (#30132, #33834, etc.) → .NET 11.0-preview3 ✅
- Preview 4 PRs (no release branch yet) → falls back to Versions.props ✅
- .NET 10 SR5/SR6 PRs → found in correct release branch ✅

88 Pester tests pass (5 new for ConvertBranchToMilestone).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen changed the title Add preview/RC milestone support to milestone drift fixer Add preview/RC milestone support with release branch detection Apr 16, 2026
github-actions Bot and others added 4 commits April 16, 2026 12:14
.NET 10 has both SR branches and legacy preview/RC branches from
its pre-GA phase. A commit reachable from both GA and a preview
should be milestoned as GA, not preview. Same for SRs vs previews.

Sort order: GA (0) → SR1-SRn (100+) → RC1-RCn (200+) → preview1-n (1000+)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previews are cut earlier in the release cycle than RCs. A commit
in both preview7 and rc1 should be milestoned preview7.

Order: GA (0) → SRs (100+) → previews (200+) → RCs (1000+)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The sort order should match the actual release timeline:
preview1 → preview7 → RC1 → RC2 → GA → SR1 → SR5

A commit reachable from both preview7 and GA should be milestoned
preview7 (where it first shipped), not GA.

Order: previews (100+) → RCs (200+) → GA (500) → SRs (1000+)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
$expectedMs and $script:_preLabel/_preIter were used before being
set when a PR's merge commit wasn't on any release branch (e.g.
PRs merged to inflight/candidate). Initialize them at function entry.

Also improved Major version derivation to parse from the milestone
name when $ReleaseTag isn't set (release branch path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen
Copy link
Copy Markdown
Member Author

PureWeen commented Apr 16, 2026

PR #34999 — Multi-Model Code Review (Final — Round 5)

CI Status: ⚠️ maui-pr skipped (script-only — expected)

Review Results

  • Reviewer 1: One question — is pull-requests: write needed? Suggests issues: write may suffice.
  • Reviewer 2: No issues. Confirms pull-requests: write is correct.
  • Reviewer 3: No issues found.

Resolution: pull-requests: write is validated by production failure — the live workflow returned HTTP 403 when setting a milestone on PR #34989 with pull-requests: read. Setting milestones on PRs (not issues) via the Issues API requires pull-requests: write.

Summary Across All Rounds

Round Findings Status
1 7 (3 moderate, 4 minor) All fixed
2 3 (2 moderate, 1 minor) All fixed
3 3 (StrictMode, fallback, warning) All fixed
4 0 new (permission question resolved) N/A
5 0 new issues ✅ Clean

Test Coverage

Recommended Action

Approve — Zero issues across 3 reviewers. All findings from 5 rounds resolved. Production-validated.

github-actions Bot and others added 3 commits April 16, 2026 13:27
- Set $ReleaseTag from versionInfo when release branch detection
  succeeds (was null, causing blank report metadata)
- Replace $script:_preLabel/_preIter with local variables (no need
  for script scope — used within single function only)
- Fix Find-ReleaseBranchForCommit docstring: says 'GA first' but
  code sorts previews first (chronological). Doc now matches code.
- Fix Get-VersionFromGitRef docstring: key names now match actual
  hashtable keys (PreLabel/PreIter not PreReleaseLabel/Iteration)
- Replace dead $isAncestor variable with $null = git pattern

88 Pester tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tag mode now works with preview tags like -Tag 11.0.0-preview.3.x:
- Reads Versions.props at the tag to get PreLabel/PreIter
- ConvertTo-Milestone produces '.NET 11.0-preview3'
- Find-PreviousTag finds preview2 as the previous tag (was returning
  null, causing full-history scan of 11K+ commits)

New Get-TagSortKey function assigns chronological sort keys:
  preview1 (101) < preview7 (107) < rc1 (201) < GA (500) < sr5 (505)

Test-IsReleaseTag now matches preview/RC tags (was stable-only).

Validated: -Tag 11.0.0-preview.3.26203.7 correctly finds 234 PRs
in the preview2..preview3 range, filters 180 merge-ups, checks 53
.NET 11 PRs, resolves to .NET 11.0-preview3.

91 Pester tests pass (5 new for preview tag ordering).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Find-TagContainingPr: use Get-TagSortKey instead of Get-PatchVersion
  (preview tags all returned 0, causing nondeterministic sort order)
- Guard $versionInfo.Tag null dereference when release branch succeeds
  but Versions.props read failed
- Explicit preview tag in single-PR mode: read Versions.props at tag
  to get PreLabel/PreIter (was throwing because ConvertTo-Milestone
  can't parse preview tag strings directly)
- Use clean version tag for ConvertTo-Milestone when $versionInfo
  is available (e.g. '11.0.0' not '11.0.0-preview.3.26203.7')

91 Pester tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions Bot and others added 2 commits April 16, 2026 14:36
- Initialize $versionInfo to $null at function entry (StrictMode crash
  when neither -Tag nor MergeCommitSha provided)
- When Find-TagContainingPr returns a preview tag and $versionInfo is
  null, read Versions.props at the tag to get clean version + pre-release
  info (was throwing because ConvertTo-Milestone can't parse preview tags)
- Warn when PreLabel is set but PreIter is 0 (would silently fall
  through to GA mapping)

91 Pester tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Setting milestones on PRs via the Issues API requires pull-requests: write,
not just read. Issues worked (issues: write) but PRs returned 403.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Apr 16, 2026

🟢 .NET MAUI Code Review — Approved

👋 @PureWeen — new code review results are available. Please review the latest session below.

🟢 Review Session — Approved7e4c1ff · Add preview/RC milestone support with release branch detection · 2026-04-16 20:46 UTC

Code Review — PR #34999

Independent Assessment

What this changes: Extends Fix-MilestoneDrift.ps1 (a PowerShell tool for auto-milestoning merged PRs) to support preview/RC milestones (e.g. .NET 11.0-preview3) and to detect which release a PR ships in by walking release branches via merge-base --is-ancestor. Adds a new Find-ReleaseBranchForCommit that checks branches in chronological order (preview → rc → GA → sr), new ConvertBranchToMilestone, a Get-TagSortKey helper so Find-PreviousTag works across preview/RC/stable, and returns Get-VersionFromGitRef as a hashtable (Tag/PreLabel/PreIter). Workflow permission bumped from pull-requests: readwrite.

Inferred motivation: PRs merged into net11.0 kept getting no milestone because Versions.props on that branch always shows PreReleaseVersionIteration=1. Checking actual release branches gives an authoritative signal.

Reconciliation with PR Narrative

Author's description matches the code precisely — validation table (8 PRs) is consistent with the release-branch-first detection order. No disagreement.

Findings

⚠️ Warning — pull-requests: write permission may be broader than needed

.github/workflows/fix-milestone-drift.yml:32. Milestones are applied via the Issues API (PATCH /issues/:number) which only requires issues: write (already granted). I don't see a code path in the script that posts review bodies, adds PR assignees, or other PR-only mutations. If this was added defensively because gh sometimes treats PR milestone updates differently, a one-line comment explaining why would help future maintainers apply least-privilege. If it's not actually required, revert to read.

⚠️ Warning — Find-ReleaseBranchForCommit does an unconditional git fetch origin $branch per branch in the sorted list

.github/scripts/Fix-MilestoneDrift.ps1 in Find-ReleaseBranchForCommit. Inside the foreach, every candidate branch triggers a network fetch before the merge-base --is-ancestor check — even though the workflow already runs actions/checkout@v4 with fetch-depth: 0. As more preview/RC/SR branches accumulate for a major version this grows linearly per workflow run. Consider a single git fetch origin 'refs/heads/release/$Major.0.*:refs/remotes/origin/release/$Major.0.*' before the loop, or skip the per-branch fetch entirely since the workflow already has full history. Low severity — correctness is fine, just wasteful.

💡 Suggestion — Test-IsReleaseTag regex is now very permissive

Changed from ^$Major\.0\.\d+$ to ^$Major\.0\.. It now accepts strings like 10.0.abc or 10.0.0-anything. In practice the input comes from Get-AllTags, so garbage is unlikely, but a slightly tighter pattern (e.g. ^$Major\.0\.(\d+$|0-(preview|rc)\.)) would preserve the preview/RC behavior without losing the original guard. Non-blocking.

💡 Suggestion — Consider anchoring Get-TagSortKey returns so adjacent ranges can't collide

Preview ranges 100–199, RC 200–299, stable 500 + patch. A stable patch ≥ 300 collides into the RC range (unlikely at MAUI's cadence, but). A trivial guard: bump stable to 1_000_000 + patch, RC to 1000 + iter, etc. Non-blocking.

Devil's Advocate

  • The release-branch-first strategy is a nice inversion: branch membership is ground truth, not Versions.props. The "earliest match wins" logic correctly handles the case where a commit lands in preview3 and later ships in GA — it gets milestoned preview3 (where it first shipped). 👍
  • The three-way fallback in Invoke-AnalyzeSinglePr (explicit tag → release branch → Versions.props → tag-range search) is clear and each step is independently testable.
  • Tests cover the new mapping functions thoroughly (88 Pester tests, 11 new). Find-ReleaseBranchForCommit itself isn't unit-tested (network-bound), but the author validated 8 real PRs in the description.

Verdict: LGTM

Confidence: high
Summary: Infra-only change to a milestone-drift tool — no MAUI runtime impact. Logic is sound, tests are comprehensive, real PRs were validated. Two minor housekeeping items (permission scope, per-branch fetch) and two non-blocking nits — all suitable as follow-ups rather than pre-merge requirements.


@PureWeen PureWeen merged commit f4fb086 into main Apr 16, 2026
4 of 5 checks passed
@PureWeen PureWeen deleted the feature/milestone-preview-support branch April 16, 2026 21:24
@github-actions github-actions Bot added this to the .NET 10 SR7 milestone Apr 16, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators May 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants