From 6d1d99016978cbc0604ffb1640a35f693cd52c7d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sat, 7 Feb 2026 01:29:08 -0600 Subject: [PATCH 1/3] Use Version.Details.xml as primary VMR snapshot source Version.Details.xml's is the authoritative record of which VMR commit a product repo branch is based on. Previously the script treated it as a fallback after commit message parsing. Now it's checked first, with commit messages as secondary confirmation. This correctly handles: - Manual backflow (darc vmr backflow pushed directly) - Normal codeflow (Maestro-managed) - Conflicted PRs (VD.xml reflects pre-codeflow state) - Forward flow PRs (skips VD.xml, uses commit messages) Tested against sdk#52727 (manual backflow) and sdk#52885 (conflicted). --- .../scripts/Get-CodeflowStatus.ps1 | 106 ++++++++++++------ 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 50fb720837ffca..817addc0fb9956 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -402,69 +402,101 @@ $freshnessRepoLabel = if ($isForwardFlow) { $sourceRepo } else { "VMR" } # Pre-load PR commits for use in validation and later analysis $prCommits = $pr.commits -# --- Step 2b: Cross-reference PR body snapshot against actual branch commits --- +# --- Step 2b: Determine actual VMR snapshot on the PR branch --- +# Priority: 1) Version.Details.xml (ground truth), 2) commit messages, 3) PR body $branchVmrCommit = $null +$commitMsgVmrCommit = $null +$versionDetailsVmrCommit = $null + +# First: check eng/Version.Details.xml on the PR branch (authoritative source) +if (-not $isForwardFlow) { + $vdContent = Invoke-GitHubApi "/repos/$Repository/contents/eng/Version.Details.xml?ref=$([System.Uri]::EscapeDataString($pr.headRefName))" -Raw + if ($vdContent) { + try { + [xml]$vdXml = $vdContent + $sourceNode = $vdXml.Dependencies.Source + if ($sourceNode -and $sourceNode.Sha -and $sourceNode.Sha -match '^[a-fA-F0-9]{40}$') { + $versionDetailsVmrCommit = $sourceNode.Sha + $branchVmrCommit = $versionDetailsVmrCommit + } + } + catch { + # Fall back to regex if XML parsing fails + if ($vdContent -match ']*Sha="([a-fA-F0-9]+)"') { + $versionDetailsVmrCommit = $Matches[1] + $branchVmrCommit = $versionDetailsVmrCommit + } + } + } +} + +# Second: scan commit messages for "Backflow from" / "Forward flow from" SHAs if ($prCommits) { - # Look through PR branch commits (newest first) for "Backflow from" or "Forward flow from" messages - # containing the actual VMR/source SHA that was used to create the branch content $reversedCommits = @($prCommits) [Array]::Reverse($reversedCommits) foreach ($c in $reversedCommits) { $msg = $c.messageHeadline - # Backflow commits: "Backflow from https://github.com/dotnet/dotnet / build " if ($msg -match '(?:Backflow|Forward flow) from .+ / ([a-fA-F0-9]+)') { - $branchVmrCommit = $Matches[1] - # Keep scanning — we want the most recent (last in original order = first in reversed) + $commitMsgVmrCommit = $Matches[1] break } } + # For forward flow (no Version.Details.xml source), commit messages are primary + if (-not $branchVmrCommit -and $commitMsgVmrCommit) { + $branchVmrCommit = $commitMsgVmrCommit + } } if ($branchVmrCommit -or $vmrCommit) { Write-Section "Snapshot Validation" $usedBranchSnapshot = $false - if ($branchVmrCommit -and $vmrCommit) { - $bodyShort = Get-ShortSha $vmrCommit - $branchShort = $branchVmrCommit # already short from commit message - if ($vmrCommit.StartsWith($branchVmrCommit) -or $branchVmrCommit.StartsWith($vmrCommit)) { - Write-Host " ✅ PR body snapshot ($bodyShort) matches branch commit ($branchShort)" -ForegroundColor Green + + if ($branchVmrCommit) { + # We have a branch-derived snapshot (from Version.Details.xml or commit message) + $branchShort = Get-ShortSha $branchVmrCommit + $sourceLabel = if ($versionDetailsVmrCommit -and $branchVmrCommit -eq $versionDetailsVmrCommit) { "Version.Details.xml" } else { "branch commit" } + + if ($vmrCommit) { + $bodyShort = Get-ShortSha $vmrCommit + if ($vmrCommit.StartsWith($branchVmrCommit) -or $branchVmrCommit.StartsWith($vmrCommit)) { + Write-Host " ✅ $sourceLabel ($branchShort) matches PR body ($bodyShort)" -ForegroundColor Green + } + else { + Write-Host " ⚠️ MISMATCH: $sourceLabel has $branchShort but PR body claims $bodyShort" -ForegroundColor Red + Write-Host " PR body is stale — using $sourceLabel for freshness check" -ForegroundColor Yellow + } + } + else { + Write-Host " ℹ️ PR body has no commit reference — using $sourceLabel ($branchShort)" -ForegroundColor Yellow + } + + # Resolve to full SHA for accurate comparison (skip API call if already full-length) + if ($branchVmrCommit.Length -ge 40) { + $vmrCommit = $branchVmrCommit + $usedBranchSnapshot = $true } else { - Write-Host " ⚠️ MISMATCH: PR body claims $(Get-ShortSha $vmrCommit) but branch commit references $branchVmrCommit" -ForegroundColor Red - Write-Host " The PR body may be stale — using branch commit ($branchVmrCommit) for freshness check" -ForegroundColor Yellow - # Resolve the short SHA from the branch commit to a full SHA for accurate comparison $resolvedCommit = Invoke-GitHubApi "/repos/$freshnessRepo/commits/$branchVmrCommit" if ($resolvedCommit) { $vmrCommit = $resolvedCommit.sha $usedBranchSnapshot = $true } + elseif ($vmrCommit) { + Write-Host " ⚠️ Could not resolve $sourceLabel SHA $branchShort — falling back to PR body ($(Get-ShortSha $vmrCommit))" -ForegroundColor Yellow + } else { - Write-Host " ⚠️ Could not resolve branch commit SHA $branchVmrCommit — falling back to PR body" -ForegroundColor Yellow + Write-Host " ⚠️ Could not resolve $sourceLabel SHA $branchShort" -ForegroundColor Yellow } } } - elseif ($branchVmrCommit -and -not $vmrCommit) { - Write-Host " ⚠️ PR body has no commit reference, but branch commit references $branchVmrCommit" -ForegroundColor Yellow - Write-Host " Using branch commit for freshness check" -ForegroundColor Yellow - $resolvedCommit = Invoke-GitHubApi "/repos/$freshnessRepo/commits/$branchVmrCommit" - if ($resolvedCommit) { - $vmrCommit = $resolvedCommit.sha - $usedBranchSnapshot = $true - } - } - elseif ($vmrCommit -and -not $branchVmrCommit) { + else { + # No branch-derived snapshot — PR body only $commitCount = if ($prCommits) { $prCommits.Count } else { 0 } - if ($commitCount -eq 1) { - $firstMsg = $prCommits[0].messageHeadline - if ($firstMsg -match "^Initial commit for subscription") { - Write-Host " ℹ️ PR has only an initial subscription commit — PR body snapshot ($(Get-ShortSha $vmrCommit)) not yet verifiable from branch" -ForegroundColor DarkGray - } - else { - Write-Host " ⚠️ No VMR SHA found in branch commit messages — trusting PR body ($(Get-ShortSha $vmrCommit))" -ForegroundColor Yellow - } + if ($commitCount -eq 1 -and $prCommits[0].messageHeadline -match "^Initial commit for subscription") { + Write-Host " ℹ️ PR has only an initial subscription commit — PR body snapshot ($(Get-ShortSha $vmrCommit)) not yet verifiable" -ForegroundColor DarkGray } else { - Write-Host " ⚠️ No VMR SHA found in $commitCount branch commit messages — trusting PR body ($(Get-ShortSha $vmrCommit))" -ForegroundColor Yellow + Write-Host " ⚠️ Could not verify PR body snapshot ($(Get-ShortSha $vmrCommit)) from branch" -ForegroundColor Yellow } } } @@ -485,7 +517,11 @@ if ($vmrCommit -and $vmrBranch) { if ($branchHead) { $sourceHeadSha = $branchHead.sha $sourceHeadDate = $branchHead.commit.committer.date - $snapshotSource = if ($usedBranchSnapshot) { "from branch commit" } else { "from PR body" } + $snapshotSource = if ($usedBranchSnapshot) { + if ($versionDetailsVmrCommit -and $vmrCommit.StartsWith($versionDetailsVmrCommit)) { "from Version.Details.xml" } + elseif ($commitMsgVmrCommit) { "from branch commit" } + else { "from branch" } + } else { "from PR body" } Write-Status "PR snapshot" "$(Get-ShortSha $vmrCommit) ($snapshotSource)" Write-Status "$freshnessRepoLabel HEAD" "$(Get-ShortSha $sourceHeadSha) ($sourceHeadDate)" From adc8cb13c249550b40494ffe841b50cafb8b31df Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sat, 7 Feb 2026 11:10:45 -0600 Subject: [PATCH 2/3] Add forward flow scanning to -CheckMissing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -CheckMissing now scans open forward flow PRs (product repo → dotnet/dotnet) in addition to missing backflow PRs. For each forward flow PR it detects: - Conflict (Maestro 'Conflict detected' comment) - Staleness (opposite codeflow merged while PR was open) - Healthy (no issues) Summary section now shows both directions. Also fixes: dotnet-maestro comment author matching (gh CLI returns 'dotnet-maestro' not 'dotnet-maestro[bot]' for login field). Tested against dotnet/sdk (4 forward PRs: 2 healthy, 1 stale, 1 conflict) and dotnet/runtime (3 forward PRs: 2 healthy, 1 stale). --- .github/skills/vmr-codeflow-status/SKILL.md | 13 +-- .../scripts/Get-CodeflowStatus.ps1 | 84 ++++++++++++++++++- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 45c0eed81c3059..16ae145c7c290b 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -5,7 +5,9 @@ description: Analyze VMR codeflow PR status for dotnet repositories. Use when in # VMR Codeflow Status -Analyze the health of VMR codeflow PRs (backflow from `dotnet/dotnet` to product repositories like `dotnet/sdk`). +Analyze the health of VMR codeflow PRs in both directions: +- **Backflow**: `dotnet/dotnet` → product repos (e.g., `dotnet/sdk`) +- **Forward flow**: product repos → `dotnet/dotnet` ## When to Use This Skill @@ -14,8 +16,8 @@ Use this skill when: - You need to check if a specific fix has flowed through the VMR pipeline to a codeflow PR - A PR has a Maestro staleness warning ("codeflow cannot continue") or conflict - You need to understand what manual commits would be lost if a codeflow PR is closed -- You want to know if expected backflow PRs are missing for a repo/branch -- Asked questions like "is this codeflow PR up to date", "has the runtime revert reached this PR", "why is the codeflow blocked" +- You want to check the overall state of flow for a repo (backflow and forward flow health) +- Asked questions like "is this codeflow PR up to date", "has the runtime revert reached this PR", "why is the codeflow blocked", "what is the state of flow for the sdk" ## Quick Start @@ -29,7 +31,7 @@ Use this skill when: # Show individual VMR commits that are missing ./scripts/Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" -ShowCommits -# Check if any backflow PRs are missing for a repo +# Check overall flow health for a repo (backflow + forward flow) ./scripts/Get-CodeflowStatus.ps1 -Repository "dotnet/roslyn" -CheckMissing # Check a specific branch only @@ -44,7 +46,7 @@ Use this skill when: | `-Repository` | Target repo in `owner/repo` format (default: `dotnet/sdk`) | | `-TraceFix` | Trace a repo PR through the pipeline (e.g., `dotnet/runtime#123974`) | | `-ShowCommits` | Show individual VMR commits between PR snapshot and branch HEAD | -| `-CheckMissing` | Check if backflow PRs are expected but missing for a repository | +| `-CheckMissing` | Check overall flow health: missing backflow PRs and forward flow PR status | | `-Branch` | With `-CheckMissing`, only check a specific branch | ## What the Script Does @@ -58,6 +60,7 @@ Use this skill when: 7. **Traces fixes** (optional) — Checks if a specific fix has flowed through VMR → codeflow PR 8. **Recommends actions** — Suggests force trigger, close/reopen, merge as-is, resolve conflicts, or wait 9. **Checks for missing backflow** (optional) — Finds branches where a backflow PR should exist but doesn't +10. **Scans forward flow** (optional) — Checks open forward flow PRs into `dotnet/dotnet` for staleness and conflicts ## Interpreting Results diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 817addc0fb9956..b264529d5ea191 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -260,14 +260,90 @@ if ($CheckMissing) { } } + # --- Forward flow: check PRs from this repo into the VMR --- + $repoShortName = $Repository -replace '^dotnet/', '' + Write-Host "" + Write-Section "Forward flow PRs ($Repository → dotnet/dotnet)" + + $fwdPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from dotnet/$repoShortName" --json number,title --limit 50 2>$null + $fwdPRs = @() + if ($LASTEXITCODE -eq 0 -and $fwdPRsJson) { + try { $fwdPRs = ($fwdPRsJson -join "`n") | ConvertFrom-Json } catch { $fwdPRs = @() } + } + # Filter to exact repo match (avoid dotnet/sdk matching dotnet/sdk-container-builds) + $fwdPRs = @($fwdPRs | Where-Object { $_.title -match "from dotnet/$([regex]::Escape($repoShortName))$" }) + + $fwdHealthy = 0 + $fwdStale = 0 + $fwdConflict = 0 + + if ($fwdPRs.Count -eq 0) { + Write-Host " No open forward flow PRs found" -ForegroundColor DarkGray + } + else { + foreach ($fpr in $fwdPRs) { + $fprBranch = if ($fpr.title -match '^\[([^\]]+)\]') { $Matches[1] } else { "unknown" } + if ($Branch -and $fprBranch -ne $Branch) { continue } + + # Get PR details for staleness/conflict check + $fprDetailJson = gh pr view $fpr.number -R dotnet/dotnet --json body,comments,updatedAt 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host " PR #$($fpr.number) [$fprBranch]: ⚠️ Could not fetch details" -ForegroundColor Yellow + continue + } + $fprDetail = ($fprDetailJson -join "`n") | ConvertFrom-Json + + # Check for staleness warnings and conflicts in comments + $hasStaleness = $false + $hasConflict = $false + if ($fprDetail.comments) { + foreach ($comment in $fprDetail.comments) { + if ($comment.author.login -match '^dotnet-maestro') { + if ($comment.body -match 'codeflow cannot continue|the source repository has received code changes') { $hasStaleness = $true } + if ($comment.body -match 'Conflict detected') { $hasConflict = $true } + } + } + } + + $status = "✅ Healthy" + $color = "Green" + if ($hasConflict) { + $status = "🔴 Conflict" + $color = "Red" + $fwdConflict++ + } + elseif ($hasStaleness) { + $status = "⚠️ Stale" + $color = "Yellow" + $fwdStale++ + } + else { + $fwdHealthy++ + } + + Write-Host " PR #$($fpr.number) [$fprBranch]: $status" -ForegroundColor $color + Write-Host " https://github.com/dotnet/dotnet/pull/$($fpr.number)" -ForegroundColor DarkGray + } + } + Write-Section "Summary" - Write-Host " Branches with open backflow PRs: $coveredCount" -ForegroundColor Green - Write-Host " Branches up to date (no PR needed): $upToDateCount" -ForegroundColor Green + Write-Host " Backflow ($Repository ← dotnet/dotnet):" -ForegroundColor White + Write-Host " Branches with open PRs: $coveredCount" -ForegroundColor Green + Write-Host " Branches up to date: $upToDateCount" -ForegroundColor Green if ($missingCount -gt 0) { - Write-Host " Branches MISSING backflow PRs: $missingCount" -ForegroundColor Red + Write-Host " Branches MISSING backflow PRs: $missingCount" -ForegroundColor Red + } + else { + Write-Host " No missing backflow PRs ✅" -ForegroundColor Green + } + Write-Host " Forward flow ($Repository → dotnet/dotnet):" -ForegroundColor White + if ($fwdPRs.Count -eq 0) { + Write-Host " No open forward flow PRs" -ForegroundColor DarkGray } else { - Write-Host " No missing backflow PRs detected ✅" -ForegroundColor Green + if ($fwdHealthy -gt 0) { Write-Host " Healthy: $fwdHealthy" -ForegroundColor Green } + if ($fwdStale -gt 0) { Write-Host " Stale: $fwdStale" -ForegroundColor Yellow } + if ($fwdConflict -gt 0) { Write-Host " Conflicted: $fwdConflict" -ForegroundColor Red } } return } From 7ed57569ce8a5658f846a99f75a7e2479bbc9a27 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sat, 7 Feb 2026 11:21:06 -0600 Subject: [PATCH 3/3] Address review: stderr redirect, JSON try/catch, SHA validation - Use 2>$null instead of 2>&1 for gh pr view to prevent stderr corrupting JSON stream - Wrap ConvertFrom-Json in try/catch for forward flow PR details - Tighten regex fallback to require exactly 40-char hex SHA - Use case-insensitive StringComparison for SHA matching --- .../scripts/Get-CodeflowStatus.ps1 | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index b264529d5ea191..aa922126c12406 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -72,7 +72,7 @@ function Invoke-GitHubApi { $args += '-H' $args += 'Accept: application/vnd.github.raw' } - $result = gh api @args 2>&1 + $result = gh api @args 2>$null if ($LASTEXITCODE -ne 0) { Write-Warning "GitHub API call failed: $Endpoint" return $null @@ -186,12 +186,18 @@ if ($CheckMissing) { } # Get the PR body to extract VMR commit and VMR branch - $prDetailJson = gh pr view $lastPR.number -R $Repository --json body 2>&1 + $prDetailJson = gh pr view $lastPR.number -R $Repository --json body 2>$null if ($LASTEXITCODE -ne 0) { Write-Host " ⚠️ Could not fetch PR #$($lastPR.number) details" -ForegroundColor Yellow continue } - $prDetail = ($prDetailJson -join "`n") | ConvertFrom-Json + try { + $prDetail = ($prDetailJson -join "`n") | ConvertFrom-Json + } + catch { + Write-Host " ⚠️ Could not parse PR #$($lastPR.number) details" -ForegroundColor Yellow + continue + } $vmrCommitFromPR = $null $vmrBranchFromPR = $null @@ -265,7 +271,7 @@ if ($CheckMissing) { Write-Host "" Write-Section "Forward flow PRs ($Repository → dotnet/dotnet)" - $fwdPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from dotnet/$repoShortName" --json number,title --limit 50 2>$null + $fwdPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from dotnet/$repoShortName" --json number,title --limit 10 2>$null $fwdPRs = @() if ($LASTEXITCODE -eq 0 -and $fwdPRsJson) { try { $fwdPRs = ($fwdPRsJson -join "`n") | ConvertFrom-Json } catch { $fwdPRs = @() } @@ -286,12 +292,18 @@ if ($CheckMissing) { if ($Branch -and $fprBranch -ne $Branch) { continue } # Get PR details for staleness/conflict check - $fprDetailJson = gh pr view $fpr.number -R dotnet/dotnet --json body,comments,updatedAt 2>&1 + $fprDetailJson = gh pr view $fpr.number -R dotnet/dotnet --json body,comments,updatedAt 2>$null if ($LASTEXITCODE -ne 0) { Write-Host " PR #$($fpr.number) [$fprBranch]: ⚠️ Could not fetch details" -ForegroundColor Yellow continue } - $fprDetail = ($fprDetailJson -join "`n") | ConvertFrom-Json + try { + $fprDetail = ($fprDetailJson -join "`n") | ConvertFrom-Json + } + catch { + Write-Host " PR #$($fpr.number) [$fprBranch]: ⚠️ Could not parse details" -ForegroundColor Yellow + continue + } # Check for staleness warnings and conflicts in comments $hasStaleness = $false @@ -498,7 +510,7 @@ if (-not $isForwardFlow) { } catch { # Fall back to regex if XML parsing fails - if ($vdContent -match ']*Sha="([a-fA-F0-9]+)"') { + if ($vdContent -match ']*Sha="([a-fA-F0-9]{40})"') { $versionDetailsVmrCommit = $Matches[1] $branchVmrCommit = $versionDetailsVmrCommit } @@ -534,7 +546,7 @@ if ($branchVmrCommit -or $vmrCommit) { if ($vmrCommit) { $bodyShort = Get-ShortSha $vmrCommit - if ($vmrCommit.StartsWith($branchVmrCommit) -or $branchVmrCommit.StartsWith($vmrCommit)) { + if ($vmrCommit.StartsWith($branchVmrCommit, [StringComparison]::OrdinalIgnoreCase) -or $branchVmrCommit.StartsWith($vmrCommit, [StringComparison]::OrdinalIgnoreCase)) { Write-Host " ✅ $sourceLabel ($branchShort) matches PR body ($bodyShort)" -ForegroundColor Green } else { @@ -594,7 +606,7 @@ if ($vmrCommit -and $vmrBranch) { $sourceHeadSha = $branchHead.sha $sourceHeadDate = $branchHead.commit.committer.date $snapshotSource = if ($usedBranchSnapshot) { - if ($versionDetailsVmrCommit -and $vmrCommit.StartsWith($versionDetailsVmrCommit)) { "from Version.Details.xml" } + if ($versionDetailsVmrCommit -and $vmrCommit.StartsWith($versionDetailsVmrCommit, [StringComparison]::OrdinalIgnoreCase)) { "from Version.Details.xml" } elseif ($commitMsgVmrCommit) { "from branch commit" } else { "from branch" } } else { "from PR body" }