From a86f37c16fc529e62b01e838f0bf3f0dc21f5fa7 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 16:14:52 -0600 Subject: [PATCH 01/19] Add vmr-codeflow-status Copilot skill Adds a Copilot Agent Skill that analyzes VMR codeflow PR health: - Parses PR metadata (subscription ID, VMR commit, build info) - Checks VMR freshness (compares PR snapshot vs branch HEAD) - Detects Maestro staleness warnings - Analyzes PR commits (auto-updates vs manual) - Traces specific fixes through VMR pipeline (-TraceFix) - Provides actionable recommendations with darc commands --- .github/skills/vmr-codeflow-status/SKILL.md | 99 ++++ .../references/vmr-codeflow-reference.md | 144 +++++ .../scripts/Get-CodeflowStatus.ps1 | 523 ++++++++++++++++++ 3 files changed, 766 insertions(+) create mode 100644 .github/skills/vmr-codeflow-status/SKILL.md create mode 100644 .github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md create mode 100644 .github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md new file mode 100644 index 00000000000000..03175d2b8f2ec7 --- /dev/null +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -0,0 +1,99 @@ +--- +name: vmr-codeflow-status +description: Analyze VMR codeflow PR status for dotnet repositories. Use when investigating stale codeflow PRs, checking if fixes have flowed through the VMR pipeline, or debugging dependency update issues in PRs authored by dotnet-maestro[bot]. +license: MIT +metadata: + author: lewing + version: "1.0" +--- + +# VMR Codeflow Status + +Analyze the health of VMR codeflow PRs (backflow from `dotnet/dotnet` to product repositories like `dotnet/sdk`). + +## When to Use This Skill + +Use this skill when: +- A codeflow PR (from `dotnet-maestro[bot]`) has failing tests and you need to know if it's stale +- 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") +- You need to understand what manual commits would be lost if a codeflow PR is closed +- Asked questions like "is this codeflow PR up to date", "has the runtime revert reached this PR", "why is the codeflow blocked" + +## Quick Start + +```powershell +# Check codeflow PR status (most common) +./scripts/Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" + +# Trace a specific fix through the pipeline +./scripts/Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" -TraceFix "dotnet/runtime#123974" + +# Show individual VMR commits that are missing +./scripts/Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" -ShowCommits +``` + +## Key Parameters + +| Parameter | Description | +|-----------|-------------| +| `-PRNumber` | GitHub PR number to analyze (required) | +| `-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 | + +## What the Script Does + +1. **Parses PR metadata** — Extracts VMR commit, subscription ID, build info from PR body +2. **Checks VMR freshness** — Compares PR's VMR snapshot against current VMR branch HEAD +3. **Detects staleness** — Finds Maestro "codeflow cannot continue" warnings +4. **Analyzes PR commits** — Categorizes as auto-updates vs manual commits +5. **Traces fixes** (optional) — Checks if a specific fix has flowed through VMR → codeflow PR +6. **Recommends actions** — Suggests force trigger, close/reopen, merge as-is, or wait + +## Interpreting Results + +### Freshness +- **✅ Up to date**: PR has the latest VMR snapshot +- **⚠️ VMR is N commits ahead**: The PR is missing updates. Check if the missing commits contain the fix you need. + +### Staleness +- **✅ No staleness warnings**: Maestro can freely update the PR +- **⚠️ Staleness warning detected**: A forward flow merged while this backflow PR was open. Maestro blocked further updates. + +### Manual Commits +Manual commits on the PR branch are at risk if the PR is closed or force-triggered. The script lists them so you can decide whether to preserve them. + +### Fix Tracing +When using `-TraceFix`: +- **✅ Fix is in VMR manifest**: The fix has flowed to the VMR +- **✅ Fix is in PR snapshot**: The codeflow PR already includes this fix +- **❌ Fix is NOT in PR snapshot**: The PR needs a codeflow update to get this fix + +## Darc Commands for Remediation + +After analyzing the codeflow status, common next steps involve `darc` commands: + +```bash +# Force trigger the subscription to get a fresh codeflow update +darc trigger-subscriptions --id --force + +# Normal trigger (only works if not stale) +darc trigger-subscriptions --id + +# Check subscription details +darc get-subscriptions --target-repo dotnet/sdk --source-repo dotnet/dotnet + +# Get BAR build details +darc get-build --id + +# Resolve codeflow conflicts locally +darc vmr resolve --subscription --build +``` + +Install darc via `eng\common\darc-init.ps1` in any arcade-enabled repository. + +## References + +- **VMR codeflow concepts**: See [references/vmr-codeflow-reference.md](references/vmr-codeflow-reference.md) +- **Codeflow PR documentation**: [dotnet/dotnet Codeflow-PRs.md](https://github.com/dotnet/dotnet/blob/main/docs/Codeflow-PRs.md) diff --git a/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md b/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md new file mode 100644 index 00000000000000..783fa5562edf57 --- /dev/null +++ b/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md @@ -0,0 +1,144 @@ +# VMR Codeflow Reference + +## Key Concepts + +### Codeflow Types +- **Backflow** (VMR → product repo): Automated PRs created by Maestro that bring VMR source updates + dependency updates into product repos (e.g., `dotnet/sdk`). These are titled `[branch] Source code updates from dotnet/dotnet`. +- **Forward flow** (product repo → VMR): Changes from product repos flowing into the VMR. These are titled `[branch] Source code updates from dotnet/`. + +### Staleness +When a product repo pushes changes to the VMR (forward flow merges) while a backflow PR is already open, Maestro blocks further codeflow updates to that PR. The bot posts a warning comment with options: +1. Merge the PR as-is, then Maestro creates a new PR with remaining changes +2. Close the PR and let Maestro open a fresh one (loses manual commits) +3. Force trigger: `darc trigger-subscriptions --id --force` (manual commits may be reverted) + +### Key Files +- **`src/source-manifest.json`** (in VMR): Tracks the exact commit SHA for each product repo synchronized into the VMR. This is the authoritative source of truth. +- **`eng/Version.Details.xml`** (in product repos): Tracks dependencies and includes a `` tag for codeflow tracking. + +## PR Body Metadata Format + +Codeflow PRs have structured metadata in their body: + +``` +[marker]: <> (Begin:) +## From https://github.com/dotnet/dotnet +- **Subscription**: [](https://maestro.dot.net/subscriptions?search=) +- **Build**: []() ([]()) +- **Date Produced**: +- **Commit**: []() +- **Commit Diff**: [...]() +- **Branch**: []() +[marker]: <> (End:) +``` + +## Darc CLI Commands + +The `darc` tool (Dependency ARcade) manages dependency flow in the .NET ecosystem. Install via `eng\common\darc-init.ps1` in any arcade-enabled repo. + +### Essential Commands for Codeflow Analysis + +#### Get subscription details +```bash +# Find all subscriptions flowing to a repo +darc get-subscriptions --target-repo dotnet/sdk --source-repo dotnet/dotnet + +# Output shows subscription ID, channel, update frequency, merge policies +``` + +#### Trigger a codeflow update +```bash +# Normal trigger (only works if not stale) +darc trigger-subscriptions --id + +# Force trigger (works even when stale, but may revert manual commits) +darc trigger-subscriptions --id --force + +# Trigger with a specific build +darc trigger-subscriptions --id --build +``` + +#### Get build information +```bash +# Get BAR build details by ID (found in PR body or AzDO logs) +darc get-build --id + +# Get latest build for a repo on a channel +darc get-latest-build --repo dotnet/dotnet --channel ".NET 11 Preview 1" +``` + +#### Check subscription health +```bash +# See if dependencies are missing subscriptions or have issues +darc get-health --channel ".NET 11 Preview 1" +``` + +#### Simulate a subscription update locally +```bash +# Dry-run to see what a subscription would update +darc update-dependencies --subscription --dry-run +``` + +### VMR-Specific Commands + +```bash +# Resolve codeflow conflicts locally +darc vmr resolve --subscription --build + +# Flow source from VMR → local repo +darc vmr backflow --subscription + +# Flow source from local repo → local VMR +darc vmr forwardflow --subscription + +# Get version (SHA) of a repo in the VMR +darc vmr get-version + +# Diff VMR vs product repos +darc vmr diff +``` + +### Halting and Restarting Dependency Flow + +- **Disable default channel**: `darc default-channel-status --disable --id ` — stops new builds from flowing +- **Disable subscription**: `darc subscription-status --disable --id ` — stops flow between specific repos +- **Pin dependency**: Add `Pinned="true"` to dependency in `Version.Details.xml` — prevents specific dependency from updating + +## API Endpoints + +### GitHub API +- PR details: `GET /repos/{owner}/{repo}/pulls/{pr_number}` +- PR comments: `GET /repos/{owner}/{repo}/issues/{pr_number}/comments` +- PR commits: `GET /repos/{owner}/{repo}/pulls/{pr_number}/commits` +- Compare commits: `GET /repos/{owner}/{repo}/compare/{base}...{head}` +- File contents: `GET /repos/{owner}/{repo}/contents/{path}?ref={branch}` + +### VMR Source Manifest +``` +GET /repos/dotnet/dotnet/contents/src/source-manifest.json?ref={branch} +``` +Returns JSON with `repositories[]` array, each having `path`, `remoteUri`, `commitSha`. + +### Maestro/BAR REST API +Base URL: `https://maestro.dot.net` +- Swagger: `https://maestro.dot.net/swagger` +- Get subscriptions: `GET /api/subscriptions` +- Get builds: `GET /api/builds` +- Get build by ID: `GET /api/builds/{id}` + +## Common Scenarios + +### 1. Codeflow is stale — a fix landed but hasn't reached the PR +**Symptoms**: Tests failing on the codeflow PR; the fix is merged in a product repo. +**Diagnosis**: Compare `source-manifest.json` on VMR branch HEAD vs the PR's VMR snapshot commit. +**Resolution**: Close PR + reopen, or force trigger the subscription. + +### 2. Opposite codeflow merged — staleness warning +**Symptoms**: Maestro bot comment saying "codeflow cannot continue". +**Diagnosis**: Check PR comments for the warning. Check if forward flow PRs merged after the backflow PR was opened. +**Resolution**: Follow the options in the bot's comment. + +### 3. Manual commits on the codeflow PR +**Symptoms**: Developers added manual fixes to unblock the PR (baseline updates, workarounds). +**Diagnosis**: Analyze PR commits to identify non-maestro commits. +**Risk**: Closing the PR loses these. Force-triggering may revert them. diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 new file mode 100644 index 00000000000000..6cf7ed20fb196a --- /dev/null +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -0,0 +1,523 @@ +<# +.SYNOPSIS + Analyzes VMR codeflow PR status for dotnet repositories. + +.DESCRIPTION + Checks whether a codeflow PR (backflow from dotnet/dotnet VMR) is up to date, + detects staleness warnings, traces specific fixes through the pipeline, and + provides actionable recommendations. + +.PARAMETER PRNumber + GitHub PR number to analyze. + +.PARAMETER Repository + Target repository (default: dotnet/sdk). Format: owner/repo. + +.PARAMETER TraceFix + Optional. A repo PR to trace through the pipeline (e.g., "dotnet/runtime#123974"). + Checks if the fix has flowed through VMR into the codeflow PR. + +.PARAMETER ShowCommits + Show individual VMR commits between the PR snapshot and current branch HEAD. + +.EXAMPLE + ./Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" + +.EXAMPLE + ./Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" -TraceFix "dotnet/runtime#123974" +#> + +param( + [Parameter(Mandatory = $true)] + [int]$PRNumber, + + [string]$Repository = "dotnet/sdk", + + [string]$TraceFix, + + [switch]$ShowCommits +) + +$ErrorActionPreference = "Stop" + +# --- Helpers --- + +function Invoke-GitHubApi { + param([string]$Endpoint) + try { + $result = gh api $Endpoint 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "GitHub API call failed: $Endpoint" + return $null + } + return $result | ConvertFrom-Json + } + catch { + Write-Warning "Error calling GitHub API: $_" + return $null + } +} + +function Get-ShortSha { + param([string]$Sha, [int]$Length = 12) + if (-not $Sha) { return "(unknown)" } + return $Sha.Substring(0, [Math]::Min($Length, $Sha.Length)) +} + +function ConvertFrom-Base64Content { + param([string]$Content) + try { + $clean = $Content -replace '\s', '' + return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($clean)) + } + catch { + Write-Warning "Failed to decode Base64 content: $_" + return $null + } +} + +function Write-Section { + param([string]$Title) + Write-Host "" + Write-Host "=== $Title ===" -ForegroundColor Cyan +} + +function Write-Status { + param([string]$Label, [string]$Value, [string]$Color = "White") + Write-Host " ${Label}: " -NoNewline + Write-Host $Value -ForegroundColor $Color +} + +# --- Parse repo owner/name --- +$repoParts = $Repository -split '/' +if ($repoParts.Count -ne 2) { + Write-Error "Repository must be in format 'owner/repo' (e.g., 'dotnet/sdk')" + return +} +$repoOwner = $repoParts[0] +$repoName = $repoParts[1] + +# --- Step 1: Get PR details --- +Write-Section "Codeflow PR #$PRNumber in $Repository" + +$pr = Invoke-GitHubApi "/repos/$Repository/pulls/$PRNumber" +if (-not $pr) { + Write-Error "Could not fetch PR #$PRNumber from $Repository" + return +} + +Write-Status "Title" $pr.title +Write-Status "State" $pr.state +Write-Status "Branch" "$($pr.head.ref) -> $($pr.base.ref)" +Write-Status "Created" $pr.created_at +Write-Status "Updated" $pr.updated_at +Write-Host " URL: $($pr.html_url)" + +# Check if this is actually a codeflow PR +$isMaestroPR = $pr.user.login -eq "dotnet-maestro[bot]" +$isCodeflowPR = $pr.title -match "Source code updates from dotnet/dotnet" +if (-not $isMaestroPR -and -not $isCodeflowPR) { + Write-Warning "This does not appear to be a codeflow PR (author: $($pr.user.login), title: $($pr.title))" + Write-Warning "Expected author 'dotnet-maestro[bot]' and title containing 'Source code updates from dotnet/dotnet'" +} + +# --- Step 2: Parse PR body metadata --- +Write-Section "Codeflow Metadata" + +$body = $pr.body + +# Extract subscription ID +$subscriptionId = $null +if ($body -match '\(Begin:([a-f0-9-]+)\)') { + $subscriptionId = $Matches[1] + Write-Status "Subscription" $subscriptionId +} + +# Extract VMR commit +$vmrCommit = $null +if ($body -match '\*\*Commit\*\*:\s*\[([a-f0-9]+)\]') { + $vmrCommit = $Matches[1] + Write-Status "VMR Commit" $vmrCommit +} + +# Extract build info +if ($body -match '\*\*Build\*\*:\s*\[([^\]]+)\]\(([^\)]+)\)') { + Write-Status "Build" "$($Matches[1])" + Write-Status "Build URL" $Matches[2] +} + +# Extract date produced +if ($body -match '\*\*Date Produced\*\*:\s*(.+)') { + Write-Status "Date Produced" $Matches[1].Trim() +} + +# Extract VMR branch +$vmrBranch = $null +if ($body -match '\*\*Branch\*\*:\s*\[([^\]]+)\]') { + $vmrBranch = $Matches[1] + Write-Status "VMR Branch" $vmrBranch +} + +# Extract commit diff +if ($body -match '\*\*Commit Diff\*\*:\s*\[([^\]]+)\]\(([^\)]+)\)') { + Write-Status "Commit Diff" $Matches[1] +} + +# Extract associated repo changes from footer +$repoChanges = @() +$changeMatches = [regex]::Matches($body, '- (https://github\.com/([^/]+/[^/]+)/compare/([a-f0-9]+)\.\.\.([a-f0-9]+))') +foreach ($m in $changeMatches) { + $repoChanges += @{ + URL = $m.Groups[1].Value + Repo = $m.Groups[2].Value + FromSha = $m.Groups[3].Value + ToSha = $m.Groups[4].Value + } +} +if ($repoChanges.Count -gt 0) { + Write-Status "Associated Repos" "$($repoChanges.Count) repos with source changes" +} + +if (-not $vmrCommit -or -not $vmrBranch) { + Write-Warning "Could not parse VMR metadata from PR body. This may not be a codeflow PR." + if (-not $vmrBranch) { + # Try to infer from the PR target branch + $vmrBranch = $pr.base.ref + Write-Status "Inferred VMR Branch" $vmrBranch "(from PR target)" + } +} + +# --- Step 3: Check VMR freshness --- +Write-Section "VMR Freshness" + +$vmrHeadSha = $null +$aheadBy = 0 + +if ($vmrCommit -and $vmrBranch) { + # Get current VMR branch HEAD + $vmrHead = Invoke-GitHubApi "/repos/dotnet/dotnet/commits/$vmrBranch" + if ($vmrHead) { + $vmrHeadSha = $vmrHead.sha + $vmrHeadDate = $vmrHead.commit.committer.date + Write-Status "PR snapshot" "$(Get-ShortSha $vmrCommit) (from PR body)" + Write-Status "VMR HEAD" "$(Get-ShortSha $vmrHeadSha) ($vmrHeadDate)" + + if ($vmrCommit -eq $vmrHeadSha -or $vmrHeadSha.StartsWith($vmrCommit)) { + Write-Host " ✅ PR is up to date with VMR branch" -ForegroundColor Green + } + else { + # Compare to find how many commits ahead the VMR is + $compare = Invoke-GitHubApi "/repos/dotnet/dotnet/compare/$(Get-ShortSha $vmrCommit)...$(Get-ShortSha $vmrHeadSha)" + if ($compare) { + $aheadBy = $compare.ahead_by + $behindBy = $compare.behind_by + Write-Host " ⚠️ VMR is $aheadBy commit(s) ahead of the PR snapshot" -ForegroundColor Yellow + + if ($ShowCommits -and $compare.commits) { + Write-Host "" + Write-Host " Commits since PR snapshot:" -ForegroundColor Yellow + foreach ($c in $compare.commits) { + $msg = ($c.commit.message -split "`n")[0] + if ($msg.Length -gt 100) { $msg = $msg.Substring(0, 97) + "..." } + $date = $c.commit.committer.date + Write-Host " $($c.sha.Substring(0,8)) $date $msg" + } + } + + # Check which repos have updates in the missing commits + $missingRepoUpdates = @() + if ($compare.commits) { + foreach ($c in $compare.commits) { + $msg = ($c.commit.message -split "`n")[0] + if ($msg -match 'Source code updates from ([^\s(]+)') { + $missingRepoUpdates += $Matches[1] + } + } + } + if ($missingRepoUpdates.Count -gt 0) { + $uniqueRepos = $missingRepoUpdates | Select-Object -Unique + Write-Host "" + Write-Host " Missing updates from: $($uniqueRepos -join ', ')" -ForegroundColor Yellow + } + } + } + } +} +else { + Write-Warning "Cannot check VMR freshness without VMR commit and branch info" +} + +# --- Step 4: Check staleness warnings --- +Write-Section "Staleness Check" + +$comments = Invoke-GitHubApi "/repos/$Repository/issues/$PRNumber/comments?per_page=100" +$stalenessWarnings = @() +$lastStalenessComment = $null + +if ($comments) { + if ($comments.Count -ge 100) { + Write-Warning "PR has 100+ comments — staleness warnings beyond the first page may be missed" + } + foreach ($comment in $comments) { + if ($comment.user.login -eq "dotnet-maestro[bot]" -and + ($comment.body -match "codeflow cannot continue" -or $comment.body -match "darc trigger-subscriptions")) { + $stalenessWarnings += $comment + $lastStalenessComment = $comment + } + } +} + +if ($stalenessWarnings.Count -gt 0) { + Write-Host " ⚠️ Staleness warning detected ($($stalenessWarnings.Count) warning(s))" -ForegroundColor Yellow + Write-Status "Latest warning" $lastStalenessComment.created_at + Write-Host " The VMR received opposite codeflow (forward flow merged) while this PR was open." -ForegroundColor Yellow + Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow + + # Extract darc commands from the warning + if ($lastStalenessComment.body -match 'darc trigger-subscriptions --id ([a-f0-9-]+)(?:\s+--force)?') { + Write-Host "" + Write-Host " Suggested commands from Maestro:" -ForegroundColor White + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-f0-9-]+)\s*\n') { + Write-Host " Normal trigger: $($Matches[1])" + } + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-f0-9-]+ --force)') { + Write-Host " Force trigger: $($Matches[1])" + } + } +} +else { + Write-Host " ✅ No staleness warnings found" -ForegroundColor Green +} + +# --- Step 5: Analyze PR branch commits --- +Write-Section "PR Branch Analysis" + +$manualCommits = @() +$prCommits = Invoke-GitHubApi "/repos/$Repository/pulls/$PRNumber/commits?per_page=100" +if ($prCommits) { + if ($prCommits.Count -ge 100) { + Write-Warning "PR has 100+ commits — commits beyond the first page may be missed" + } + $maestroCommits = @() + $manualCommits = @() + $mergeCommits = @() + + foreach ($c in $prCommits) { + $msg = ($c.commit.message -split "`n")[0] + $author = $c.commit.author.name + + if ($msg -match "^Merge branch") { + $mergeCommits += $c + } + elseif ($author -eq "dotnet-maestro[bot]" -or $msg -eq "Update dependencies") { + $maestroCommits += $c + } + else { + $manualCommits += $c + } + } + + Write-Status "Total commits" $prCommits.Count + Write-Status "Maestro auto-updates" $maestroCommits.Count + Write-Status "Merge commits" $mergeCommits.Count + Write-Status "Manual commits" $manualCommits.Count "$(if ($manualCommits.Count -gt 0) { 'Yellow' } else { 'Green' })" + + if ($manualCommits.Count -gt 0) { + Write-Host "" + Write-Host " Manual commits (at risk if PR is closed/force-triggered):" -ForegroundColor Yellow + foreach ($c in $manualCommits) { + $msg = ($c.commit.message -split "`n")[0] + if ($msg.Length -gt 80) { $msg = $msg.Substring(0, 77) + "..." } + Write-Host " $($c.sha.Substring(0,8)) [$($c.commit.author.name)] $msg" + } + } +} + +# --- Step 6: Trace a specific fix (optional) --- +if ($TraceFix) { + Write-Section "Tracing Fix: $TraceFix" + + # Parse TraceFix format: "owner/repo#number" or "repo#number" + $traceMatch = [regex]::Match($TraceFix, '(?:([^/]+)/)?([^#]+)#(\d+)') + if (-not $traceMatch.Success) { + Write-Warning "Could not parse TraceFix format. Expected: 'owner/repo#number' or 'repo#number'" + } + else { + $traceOwner = if ($traceMatch.Groups[1].Value) { $traceMatch.Groups[1].Value } else { "dotnet" } + $traceRepo = $traceMatch.Groups[2].Value + $traceNumber = $traceMatch.Groups[3].Value + $traceFullRepo = "$traceOwner/$traceRepo" + + # Check if the fix PR is merged + $fixPR = Invoke-GitHubApi "/repos/$traceFullRepo/pulls/$traceNumber" + if ($fixPR) { + Write-Status "Fix PR" "${traceFullRepo}#${traceNumber}: $($fixPR.title)" + Write-Status "State" $fixPR.state + Write-Status "Merged" "$(if ($fixPR.merged) { '✅ Yes' } else { '❌ No' })" "$(if ($fixPR.merged) { 'Green' } else { 'Red' })" + if ($fixPR.merged) { + Write-Status "Merged at" $fixPR.merged_at + Write-Status "Merge commit" $fixPR.merge_commit_sha + $fixMergeCommit = $fixPR.merge_commit_sha + $fixTargetBranch = $fixPR.base.ref + } + } + + # Check if the fix is in the VMR source-manifest.json on the target branch + if ($fixPR.merged -and $vmrBranch) { + Write-Host "" + Write-Host " Checking VMR source-manifest.json on $vmrBranch..." -ForegroundColor White + + $manifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$vmrBranch" + $manifestResponse = Invoke-GitHubApi $manifestUrl + if ($manifestResponse -and $manifestResponse.content) { + $manifestJson = ConvertFrom-Base64Content $manifestResponse.content + if (-not $manifestJson) { + Write-Warning "Failed to decode source-manifest.json from VMR branch" + } + else { + $manifest = $manifestJson | ConvertFrom-Json + + # Find the repo in the manifest + $escapedRepo = [regex]::Escape($traceRepo) + $repoEntry = $manifest.repositories | Where-Object { + $_.remoteUri -match "${escapedRepo}(\.git)?$" -or $_.path -eq $traceRepo + } + + if ($repoEntry) { + $manifestCommit = $repoEntry.commitSha + Write-Status "VMR manifest commit" "$(Get-ShortSha $manifestCommit) for $($repoEntry.path)" + + # Check if the fix merge commit is an ancestor of the manifest commit + if ($fixMergeCommit -eq $manifestCommit) { + Write-Host " ✅ Fix merge commit IS the VMR manifest commit" -ForegroundColor Green + } + else { + # Check if fix is an ancestor of the manifest commit + $ancestorCheck = Invoke-GitHubApi "/repos/$traceFullRepo/compare/$(Get-ShortSha $fixMergeCommit)...$(Get-ShortSha $manifestCommit)" + if ($ancestorCheck) { + if ($ancestorCheck.status -eq "ahead" -or $ancestorCheck.status -eq "identical") { + Write-Host " ✅ Fix is included in VMR manifest (manifest is ahead or identical)" -ForegroundColor Green + } + elseif ($ancestorCheck.status -eq "behind") { + Write-Host " ❌ Fix is NOT in VMR manifest yet (manifest is behind the fix)" -ForegroundColor Red + } + else { + Write-Host " ⚠️ Fix and manifest have diverged (status: $($ancestorCheck.status))" -ForegroundColor Yellow + } + } + } + + # Now check if the PR's VMR snapshot includes this + if ($vmrCommit) { + Write-Host "" + Write-Host " Checking if fix is in the PR's VMR snapshot..." -ForegroundColor White + + $snapshotManifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$vmrCommit" + $snapshotManifest = Invoke-GitHubApi $snapshotManifestUrl + if ($snapshotManifest -and $snapshotManifest.content) { + $snapshotJson = ConvertFrom-Base64Content $snapshotManifest.content + if ($snapshotJson) { + $snapshotData = $snapshotJson | ConvertFrom-Json + + $snapshotEntry = $snapshotData.repositories | Where-Object { + $_.remoteUri -match "${escapedRepo}(\.git)?$" -or $_.path -eq $traceRepo + } + + if ($snapshotEntry) { + $snapshotCommit = $snapshotEntry.commitSha + Write-Status "PR snapshot commit" "$(Get-ShortSha $snapshotCommit) for $($snapshotEntry.path)" + + if ($snapshotCommit -eq $fixMergeCommit) { + Write-Host " ✅ Fix IS in the PR's VMR snapshot" -ForegroundColor Green + } + else { + $snapshotCheck = Invoke-GitHubApi "/repos/$traceFullRepo/compare/$(Get-ShortSha $fixMergeCommit)...$(Get-ShortSha $snapshotCommit)" + if ($snapshotCheck) { + if ($snapshotCheck.status -eq "ahead" -or $snapshotCheck.status -eq "identical") { + Write-Host " ✅ Fix is included in PR snapshot" -ForegroundColor Green + } + else { + Write-Host " ❌ Fix is NOT in the PR's VMR snapshot" -ForegroundColor Red + Write-Host " The PR needs a codeflow update to pick up this fix." -ForegroundColor Yellow + } + } + } + } + } + } + } + } + else { + Write-Warning "Could not find $traceRepo in VMR source-manifest.json" + } + } + } + } + } +} + +# --- Step 7: Recommendations --- +Write-Section "Recommendations" + +$issues = @() +$recommendations = @() + +# Summarize issues +if ($stalenessWarnings.Count -gt 0) { + $issues += "Staleness warning active — codeflow is blocked" +} + +if ($vmrCommit -and $vmrHead -and $vmrCommit -ne $vmrHeadSha -and -not $vmrHeadSha.StartsWith($vmrCommit)) { + $issues += "VMR branch is ahead of PR snapshot ($aheadBy commits behind)" +} + +if ($manualCommits -and $manualCommits.Count -gt 0) { + $issues += "$($manualCommits.Count) manual commit(s) on PR branch" +} + +if ($issues.Count -eq 0) { + Write-Host " ✅ CODEFLOW HEALTHY" -ForegroundColor Green + Write-Host " The PR appears to be up to date with no issues detected." +} +else { + Write-Host " ⚠️ CODEFLOW NEEDS ATTENTION" -ForegroundColor Yellow + Write-Host "" + Write-Host " Issues:" -ForegroundColor White + foreach ($issue in $issues) { + Write-Host " • $issue" -ForegroundColor Yellow + } + + Write-Host "" + Write-Host " Options:" -ForegroundColor White + + if ($stalenessWarnings.Count -gt 0 -and $manualCommits.Count -gt 0) { + Write-Host " 1. Merge as-is — keep manual commits, get remaining changes in next codeflow PR" -ForegroundColor White + Write-Host " 2. Force trigger — updates codeflow but may revert manual commits" -ForegroundColor White + if ($subscriptionId) { + Write-Host " darc trigger-subscriptions --id $subscriptionId --force" -ForegroundColor DarkGray + } + Write-Host " 3. Close & reopen — loses manual commits, gets fresh codeflow" -ForegroundColor White + } + elseif ($stalenessWarnings.Count -gt 0) { + Write-Host " 1. Merge as-is — get remaining changes in next codeflow PR" -ForegroundColor White + Write-Host " 2. Close & reopen — gets fresh codeflow with all updates" -ForegroundColor White + Write-Host " 3. Force trigger — forces codeflow update into this PR" -ForegroundColor White + if ($subscriptionId) { + Write-Host " darc trigger-subscriptions --id $subscriptionId --force" -ForegroundColor DarkGray + } + } + elseif ($manualCommits.Count -gt 0) { + Write-Host " 1. Wait — Maestro should auto-update (if not stale)" -ForegroundColor White + Write-Host " 2. Trigger manually — if auto-updates seem delayed" -ForegroundColor White + if ($subscriptionId) { + Write-Host " darc trigger-subscriptions --id $subscriptionId" -ForegroundColor DarkGray + } + } + else { + Write-Host " 1. Wait — Maestro should auto-update the PR" -ForegroundColor White + Write-Host " 2. Trigger manually — if auto-updates seem delayed" -ForegroundColor White + if ($subscriptionId) { + Write-Host " darc trigger-subscriptions --id $subscriptionId" -ForegroundColor DarkGray + } + } +} From 66f722be6343c883f5b33e7db910138d26fee5ea Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 16:28:48 -0600 Subject: [PATCH 02/19] Fix bugs and simplify API calls Bug fixes: - Use full SHAs in compare API calls (Get-ShortSha for display only) - Fix Write-Status 3rd arg bug that would throw on color parse - Remove dead code - Handle behind/diverged status in VMR compare, not just ahead - Remove extra frontmatter fields to match existing skills Simplifications: - Replace 3 separate REST calls with single gh pr view --json - Use raw content header for source-manifest.json (no Base64) --- .github/skills/vmr-codeflow-status/SKILL.md | 4 - .../scripts/Get-CodeflowStatus.ps1 | 141 +++++++++--------- 2 files changed, 72 insertions(+), 73 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 03175d2b8f2ec7..7eb104b4f5b4bb 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -1,10 +1,6 @@ --- name: vmr-codeflow-status description: Analyze VMR codeflow PR status for dotnet repositories. Use when investigating stale codeflow PRs, checking if fixes have flowed through the VMR pipeline, or debugging dependency update issues in PRs authored by dotnet-maestro[bot]. -license: MIT -metadata: - author: lewing - version: "1.0" --- # VMR Codeflow Status diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 6cf7ed20fb196a..622d1ae2eadb3b 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -43,13 +43,22 @@ $ErrorActionPreference = "Stop" # --- Helpers --- function Invoke-GitHubApi { - param([string]$Endpoint) + param( + [string]$Endpoint, + [switch]$Raw + ) try { - $result = gh api $Endpoint 2>&1 + $args = @($Endpoint) + if ($Raw) { + $args += '-H' + $args += 'Accept: application/vnd.github.raw+json' + } + $result = gh api @args 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "GitHub API call failed: $Endpoint" return $null } + if ($Raw) { return $result -join "`n" } return $result | ConvertFrom-Json } catch { @@ -64,18 +73,6 @@ function Get-ShortSha { return $Sha.Substring(0, [Math]::Min($Length, $Sha.Length)) } -function ConvertFrom-Base64Content { - param([string]$Content) - try { - $clean = $Content -replace '\s', '' - return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($clean)) - } - catch { - Write-Warning "Failed to decode Base64 content: $_" - return $null - } -} - function Write-Section { param([string]$Title) Write-Host "" @@ -94,30 +91,29 @@ if ($repoParts.Count -ne 2) { Write-Error "Repository must be in format 'owner/repo' (e.g., 'dotnet/sdk')" return } -$repoOwner = $repoParts[0] -$repoName = $repoParts[1] -# --- Step 1: Get PR details --- +# --- Step 1: Get PR details (single call for PR + comments + commits) --- Write-Section "Codeflow PR #$PRNumber in $Repository" -$pr = Invoke-GitHubApi "/repos/$Repository/pulls/$PRNumber" -if (-not $pr) { +$prJson = gh pr view $PRNumber -R $Repository --json body,title,state,author,headRefName,baseRefName,createdAt,updatedAt,url,comments,commits 2>&1 +if ($LASTEXITCODE -ne 0) { Write-Error "Could not fetch PR #$PRNumber from $Repository" return } +$pr = $prJson | ConvertFrom-Json Write-Status "Title" $pr.title Write-Status "State" $pr.state -Write-Status "Branch" "$($pr.head.ref) -> $($pr.base.ref)" -Write-Status "Created" $pr.created_at -Write-Status "Updated" $pr.updated_at -Write-Host " URL: $($pr.html_url)" +Write-Status "Branch" "$($pr.headRefName) -> $($pr.baseRefName)" +Write-Status "Created" $pr.createdAt +Write-Status "Updated" $pr.updatedAt +Write-Host " URL: $($pr.url)" # Check if this is actually a codeflow PR -$isMaestroPR = $pr.user.login -eq "dotnet-maestro[bot]" +$isMaestroPR = $pr.author.login -eq "dotnet-maestro[bot]" $isCodeflowPR = $pr.title -match "Source code updates from dotnet/dotnet" if (-not $isMaestroPR -and -not $isCodeflowPR) { - Write-Warning "This does not appear to be a codeflow PR (author: $($pr.user.login), title: $($pr.title))" + Write-Warning "This does not appear to be a codeflow PR (author: $($pr.author.login), title: $($pr.title))" Write-Warning "Expected author 'dotnet-maestro[bot]' and title containing 'Source code updates from dotnet/dotnet'" } @@ -182,8 +178,8 @@ if (-not $vmrCommit -or -not $vmrBranch) { Write-Warning "Could not parse VMR metadata from PR body. This may not be a codeflow PR." if (-not $vmrBranch) { # Try to infer from the PR target branch - $vmrBranch = $pr.base.ref - Write-Status "Inferred VMR Branch" $vmrBranch "(from PR target)" + $vmrBranch = $pr.baseRefName + Write-Status "Inferred VMR Branch" "$vmrBranch (from PR target)" } } @@ -202,20 +198,40 @@ if ($vmrCommit -and $vmrBranch) { Write-Status "PR snapshot" "$(Get-ShortSha $vmrCommit) (from PR body)" Write-Status "VMR HEAD" "$(Get-ShortSha $vmrHeadSha) ($vmrHeadDate)" - if ($vmrCommit -eq $vmrHeadSha -or $vmrHeadSha.StartsWith($vmrCommit)) { + if ($vmrCommit -eq $vmrHeadSha) { Write-Host " ✅ PR is up to date with VMR branch" -ForegroundColor Green } else { - # Compare to find how many commits ahead the VMR is - $compare = Invoke-GitHubApi "/repos/dotnet/dotnet/compare/$(Get-ShortSha $vmrCommit)...$(Get-ShortSha $vmrHeadSha)" + # Compare to find how many commits differ between the PR snapshot and the VMR + $compare = Invoke-GitHubApi "/repos/dotnet/dotnet/compare/$vmrCommit...$vmrHeadSha" if ($compare) { $aheadBy = $compare.ahead_by $behindBy = $compare.behind_by - Write-Host " ⚠️ VMR is $aheadBy commit(s) ahead of the PR snapshot" -ForegroundColor Yellow + $compareStatus = $compare.status + + switch ($compareStatus) { + 'ahead' { + Write-Host " ⚠️ VMR is $aheadBy commit(s) ahead of the PR snapshot" -ForegroundColor Yellow + } + 'behind' { + Write-Host " ⚠️ VMR is $behindBy commit(s) behind the PR snapshot" -ForegroundColor Yellow + } + 'diverged' { + Write-Host " ⚠️ VMR and PR snapshot have diverged: VMR is $aheadBy commit(s) ahead and $behindBy commit(s) behind" -ForegroundColor Yellow + } + default { + Write-Host " ⚠️ VMR and PR snapshot differ (status: $compareStatus)" -ForegroundColor Yellow + } + } if ($ShowCommits -and $compare.commits) { Write-Host "" - Write-Host " Commits since PR snapshot:" -ForegroundColor Yellow + $commitLabel = switch ($compareStatus) { + 'ahead' { "Commits since PR snapshot:" } + 'behind' { "Commits in PR snapshot but not in VMR:" } + default { "Commits differing between VMR and PR snapshot:" } + } + Write-Host " $commitLabel" -ForegroundColor Yellow foreach ($c in $compare.commits) { $msg = ($c.commit.message -split "`n")[0] if ($msg.Length -gt 100) { $msg = $msg.Substring(0, 97) + "..." } @@ -247,29 +263,27 @@ else { Write-Warning "Cannot check VMR freshness without VMR commit and branch info" } -# --- Step 4: Check staleness warnings --- +# --- Step 4: Check staleness warnings (using comments from gh pr view) --- Write-Section "Staleness Check" -$comments = Invoke-GitHubApi "/repos/$Repository/issues/$PRNumber/comments?per_page=100" $stalenessWarnings = @() $lastStalenessComment = $null -if ($comments) { - if ($comments.Count -ge 100) { - Write-Warning "PR has 100+ comments — staleness warnings beyond the first page may be missed" - } - foreach ($comment in $comments) { - if ($comment.user.login -eq "dotnet-maestro[bot]" -and - ($comment.body -match "codeflow cannot continue" -or $comment.body -match "darc trigger-subscriptions")) { - $stalenessWarnings += $comment - $lastStalenessComment = $comment +if ($pr.comments) { + foreach ($comment in $pr.comments) { + $commentAuthor = $comment.author.login + if ($commentAuthor -eq "dotnet-maestro[bot]" -or $commentAuthor -eq "dotnet-maestro") { + if ($comment.body -match "codeflow cannot continue" -or $comment.body -match "darc trigger-subscriptions") { + $stalenessWarnings += $comment + $lastStalenessComment = $comment + } } } } if ($stalenessWarnings.Count -gt 0) { Write-Host " ⚠️ Staleness warning detected ($($stalenessWarnings.Count) warning(s))" -ForegroundColor Yellow - Write-Status "Latest warning" $lastStalenessComment.created_at + Write-Status "Latest warning" $lastStalenessComment.createdAt Write-Host " The VMR received opposite codeflow (forward flow merged) while this PR was open." -ForegroundColor Yellow Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow @@ -289,22 +303,19 @@ else { Write-Host " ✅ No staleness warnings found" -ForegroundColor Green } -# --- Step 5: Analyze PR branch commits --- +# --- Step 5: Analyze PR branch commits (using commits from gh pr view) --- Write-Section "PR Branch Analysis" $manualCommits = @() -$prCommits = Invoke-GitHubApi "/repos/$Repository/pulls/$PRNumber/commits?per_page=100" +$prCommits = $pr.commits if ($prCommits) { - if ($prCommits.Count -ge 100) { - Write-Warning "PR has 100+ commits — commits beyond the first page may be missed" - } $maestroCommits = @() $manualCommits = @() $mergeCommits = @() foreach ($c in $prCommits) { - $msg = ($c.commit.message -split "`n")[0] - $author = $c.commit.author.name + $msg = $c.messageHeadline + $author = if ($c.authors -and $c.authors.Count -gt 0) { $c.authors[0].name } else { "unknown" } if ($msg -match "^Merge branch") { $mergeCommits += $c @@ -326,9 +337,10 @@ if ($prCommits) { Write-Host "" Write-Host " Manual commits (at risk if PR is closed/force-triggered):" -ForegroundColor Yellow foreach ($c in $manualCommits) { - $msg = ($c.commit.message -split "`n")[0] + $msg = $c.messageHeadline if ($msg.Length -gt 80) { $msg = $msg.Substring(0, 77) + "..." } - Write-Host " $($c.sha.Substring(0,8)) [$($c.commit.author.name)] $msg" + $authorName = if ($c.authors -and $c.authors.Count -gt 0) { $c.authors[0].name } else { "unknown" } + Write-Host " $(Get-ShortSha $c.oid 8) [$authorName] $msg" } } } @@ -368,13 +380,8 @@ if ($TraceFix) { Write-Host " Checking VMR source-manifest.json on $vmrBranch..." -ForegroundColor White $manifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$vmrBranch" - $manifestResponse = Invoke-GitHubApi $manifestUrl - if ($manifestResponse -and $manifestResponse.content) { - $manifestJson = ConvertFrom-Base64Content $manifestResponse.content - if (-not $manifestJson) { - Write-Warning "Failed to decode source-manifest.json from VMR branch" - } - else { + $manifestJson = Invoke-GitHubApi $manifestUrl -Raw + if ($manifestJson) { $manifest = $manifestJson | ConvertFrom-Json # Find the repo in the manifest @@ -393,7 +400,7 @@ if ($TraceFix) { } else { # Check if fix is an ancestor of the manifest commit - $ancestorCheck = Invoke-GitHubApi "/repos/$traceFullRepo/compare/$(Get-ShortSha $fixMergeCommit)...$(Get-ShortSha $manifestCommit)" + $ancestorCheck = Invoke-GitHubApi "/repos/$traceFullRepo/compare/$fixMergeCommit...$manifestCommit" if ($ancestorCheck) { if ($ancestorCheck.status -eq "ahead" -or $ancestorCheck.status -eq "identical") { Write-Host " ✅ Fix is included in VMR manifest (manifest is ahead or identical)" -ForegroundColor Green @@ -413,10 +420,8 @@ if ($TraceFix) { Write-Host " Checking if fix is in the PR's VMR snapshot..." -ForegroundColor White $snapshotManifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$vmrCommit" - $snapshotManifest = Invoke-GitHubApi $snapshotManifestUrl - if ($snapshotManifest -and $snapshotManifest.content) { - $snapshotJson = ConvertFrom-Base64Content $snapshotManifest.content - if ($snapshotJson) { + $snapshotJson = Invoke-GitHubApi $snapshotManifestUrl -Raw + if ($snapshotJson) { $snapshotData = $snapshotJson | ConvertFrom-Json $snapshotEntry = $snapshotData.repositories | Where-Object { @@ -431,7 +436,7 @@ if ($TraceFix) { Write-Host " ✅ Fix IS in the PR's VMR snapshot" -ForegroundColor Green } else { - $snapshotCheck = Invoke-GitHubApi "/repos/$traceFullRepo/compare/$(Get-ShortSha $fixMergeCommit)...$(Get-ShortSha $snapshotCommit)" + $snapshotCheck = Invoke-GitHubApi "/repos/$traceFullRepo/compare/$fixMergeCommit...$snapshotCommit" if ($snapshotCheck) { if ($snapshotCheck.status -eq "ahead" -or $snapshotCheck.status -eq "identical") { Write-Host " ✅ Fix is included in PR snapshot" -ForegroundColor Green @@ -443,14 +448,12 @@ if ($TraceFix) { } } } - } } } } else { Write-Warning "Could not find $traceRepo in VMR source-manifest.json" } - } } } } @@ -467,7 +470,7 @@ if ($stalenessWarnings.Count -gt 0) { $issues += "Staleness warning active — codeflow is blocked" } -if ($vmrCommit -and $vmrHead -and $vmrCommit -ne $vmrHeadSha -and -not $vmrHeadSha.StartsWith($vmrCommit)) { +if ($vmrCommit -and $vmrHeadSha -and $vmrCommit -ne $vmrHeadSha) { $issues += "VMR branch is ahead of PR snapshot ($aheadBy commits behind)" } From a2b65562fc7ebe6cdc6bfa4472dced642d6bfbb6 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 16:37:21 -0600 Subject: [PATCH 03/19] Fix remaining review issues - Use merged_at instead of merged boolean for fix PR detection - URL-encode VMR branch names with / in API paths - Fix Accept header: application/vnd.github.raw (not raw+json) - Fix contradictory recommendation message to use compare status - Initialize behindBy/compareStatus at script scope --- .../scripts/Get-CodeflowStatus.ps1 | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 622d1ae2eadb3b..8824a0d6e2aad5 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -51,7 +51,7 @@ function Invoke-GitHubApi { $args = @($Endpoint) if ($Raw) { $args += '-H' - $args += 'Accept: application/vnd.github.raw+json' + $args += 'Accept: application/vnd.github.raw' } $result = gh api @args 2>&1 if ($LASTEXITCODE -ne 0) { @@ -188,10 +188,13 @@ Write-Section "VMR Freshness" $vmrHeadSha = $null $aheadBy = 0 +$behindBy = 0 +$compareStatus = $null if ($vmrCommit -and $vmrBranch) { - # Get current VMR branch HEAD - $vmrHead = Invoke-GitHubApi "/repos/dotnet/dotnet/commits/$vmrBranch" + # Get current VMR branch HEAD (URL-encode branch name for path segments with /) + $encodedBranch = [uri]::EscapeDataString($vmrBranch) + $vmrHead = Invoke-GitHubApi "/repos/dotnet/dotnet/commits/$encodedBranch" if ($vmrHead) { $vmrHeadSha = $vmrHead.sha $vmrHeadDate = $vmrHead.commit.committer.date @@ -360,13 +363,15 @@ if ($TraceFix) { $traceNumber = $traceMatch.Groups[3].Value $traceFullRepo = "$traceOwner/$traceRepo" - # Check if the fix PR is merged + # Check if the fix PR is merged (use merged_at since REST may not include merged boolean) $fixPR = Invoke-GitHubApi "/repos/$traceFullRepo/pulls/$traceNumber" + $fixIsMerged = $false if ($fixPR) { + $fixIsMerged = $null -ne $fixPR.merged_at Write-Status "Fix PR" "${traceFullRepo}#${traceNumber}: $($fixPR.title)" Write-Status "State" $fixPR.state - Write-Status "Merged" "$(if ($fixPR.merged) { '✅ Yes' } else { '❌ No' })" "$(if ($fixPR.merged) { 'Green' } else { 'Red' })" - if ($fixPR.merged) { + Write-Status "Merged" "$(if ($fixIsMerged) { '✅ Yes' } else { '❌ No' })" "$(if ($fixIsMerged) { 'Green' } else { 'Red' })" + if ($fixIsMerged) { Write-Status "Merged at" $fixPR.merged_at Write-Status "Merge commit" $fixPR.merge_commit_sha $fixMergeCommit = $fixPR.merge_commit_sha @@ -375,7 +380,7 @@ if ($TraceFix) { } # Check if the fix is in the VMR source-manifest.json on the target branch - if ($fixPR.merged -and $vmrBranch) { + if ($fixIsMerged -and $vmrBranch) { Write-Host "" Write-Host " Checking VMR source-manifest.json on $vmrBranch..." -ForegroundColor White @@ -471,7 +476,12 @@ if ($stalenessWarnings.Count -gt 0) { } if ($vmrCommit -and $vmrHeadSha -and $vmrCommit -ne $vmrHeadSha) { - $issues += "VMR branch is ahead of PR snapshot ($aheadBy commits behind)" + switch ($compareStatus) { + 'ahead' { $issues += "VMR is $aheadBy commit(s) ahead of PR snapshot" } + 'behind' { $issues += "VMR is $behindBy commit(s) behind PR snapshot" } + 'diverged' { $issues += "VMR and PR snapshot diverged ($aheadBy ahead, $behindBy behind)" } + default { $issues += "VMR and PR snapshot differ" } + } } if ($manualCommits -and $manualCommits.Count -gt 0) { From fb581d2cdafba603a311ab702737f18d368d5691 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 16:45:14 -0600 Subject: [PATCH 04/19] Remove dead code and join output before ConvertFrom-Json - Remove unused \ and \ variables - Join gh api / gh pr view output before ConvertFrom-Json for robustness --- .../vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 8824a0d6e2aad5..c284ccd5261481 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -59,7 +59,7 @@ function Invoke-GitHubApi { return $null } if ($Raw) { return $result -join "`n" } - return $result | ConvertFrom-Json + return ($result -join "`n") | ConvertFrom-Json } catch { Write-Warning "Error calling GitHub API: $_" @@ -100,7 +100,7 @@ if ($LASTEXITCODE -ne 0) { Write-Error "Could not fetch PR #$PRNumber from $Repository" return } -$pr = $prJson | ConvertFrom-Json +$pr = ($prJson -join "`n") | ConvertFrom-Json Write-Status "Title" $pr.title Write-Status "State" $pr.state @@ -375,7 +375,6 @@ if ($TraceFix) { Write-Status "Merged at" $fixPR.merged_at Write-Status "Merge commit" $fixPR.merge_commit_sha $fixMergeCommit = $fixPR.merge_commit_sha - $fixTargetBranch = $fixPR.base.ref } } @@ -468,7 +467,6 @@ if ($TraceFix) { Write-Section "Recommendations" $issues = @() -$recommendations = @() # Summarize issues if ($stalenessWarnings.Count -gt 0) { From 3db9577f17e87791b8742d001747ad12371cf30d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 16:46:50 -0600 Subject: [PATCH 05/19] Add forward flow support - Auto-detect flow direction from PR title - Backflow: 'Source code updates from dotnet/dotnet' (VMR -> product repo) - Forward flow: 'Source code updates from dotnet/' (repo -> VMR) - Adjust freshness comparison to use correct source repo - Use flow-aware labels (VMR Commit vs Source Commit, etc.) --- .../scripts/Get-CodeflowStatus.ps1 | 94 ++++++++++++------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index c284ccd5261481..92c9f22ffdbc07 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -109,12 +109,23 @@ Write-Status "Created" $pr.createdAt Write-Status "Updated" $pr.updatedAt Write-Host " URL: $($pr.url)" -# Check if this is actually a codeflow PR +# Check if this is actually a codeflow PR and detect flow direction $isMaestroPR = $pr.author.login -eq "dotnet-maestro[bot]" -$isCodeflowPR = $pr.title -match "Source code updates from dotnet/dotnet" -if (-not $isMaestroPR -and -not $isCodeflowPR) { +$isBackflow = $pr.title -match "Source code updates from dotnet/dotnet" +$isForwardFlow = $pr.title -match "Source code updates from (dotnet/\S+)" -and -not $isBackflow +$flowDirection = if ($isBackflow) { "backflow" } elseif ($isForwardFlow) { "forward" } else { "unknown" } + +if (-not $isMaestroPR -and -not $isBackflow -and -not $isForwardFlow) { Write-Warning "This does not appear to be a codeflow PR (author: $($pr.author.login), title: $($pr.title))" - Write-Warning "Expected author 'dotnet-maestro[bot]' and title containing 'Source code updates from dotnet/dotnet'" + Write-Warning "Expected author 'dotnet-maestro[bot]' and title containing 'Source code updates from'" +} + +if ($isForwardFlow) { + $sourceRepo = $Matches[1] + Write-Status "Flow" "Forward ($sourceRepo → $Repository)" "Cyan" +} +elseif ($isBackflow) { + Write-Status "Flow" "Backflow (dotnet/dotnet → $Repository)" "Cyan" } # --- Step 2: Parse PR body metadata --- @@ -129,12 +140,15 @@ if ($body -match '\(Begin:([a-f0-9-]+)\)') { Write-Status "Subscription" $subscriptionId } -# Extract VMR commit -$vmrCommit = $null +# Extract source commit (VMR commit for backflow, repo commit for forward flow) +$sourceCommit = $null if ($body -match '\*\*Commit\*\*:\s*\[([a-f0-9]+)\]') { - $vmrCommit = $Matches[1] - Write-Status "VMR Commit" $vmrCommit + $sourceCommit = $Matches[1] + $commitLabel = if ($isForwardFlow) { "Source Commit" } else { "VMR Commit" } + Write-Status $commitLabel $sourceCommit } +# Keep $vmrCommit alias for backflow compatibility +$vmrCommit = $sourceCommit # Extract build info if ($body -match '\*\*Build\*\*:\s*\[([^\]]+)\]\(([^\)]+)\)') { @@ -147,11 +161,12 @@ if ($body -match '\*\*Date Produced\*\*:\s*(.+)') { Write-Status "Date Produced" $Matches[1].Trim() } -# Extract VMR branch +# Extract source branch $vmrBranch = $null if ($body -match '\*\*Branch\*\*:\s*\[([^\]]+)\]') { $vmrBranch = $Matches[1] - Write-Status "VMR Branch" $vmrBranch + $branchLabel = if ($isForwardFlow) { "Source Branch" } else { "VMR Branch" } + Write-Status $branchLabel $vmrBranch } # Extract commit diff @@ -183,30 +198,36 @@ if (-not $vmrCommit -or -not $vmrBranch) { } } -# --- Step 3: Check VMR freshness --- -Write-Section "VMR Freshness" +# --- Step 3: Check source freshness --- +$freshnessLabel = if ($isForwardFlow) { "Source Freshness" } else { "VMR Freshness" } +Write-Section $freshnessLabel -$vmrHeadSha = $null +$sourceHeadSha = $null $aheadBy = 0 $behindBy = 0 $compareStatus = $null +# For backflow: compare against VMR (dotnet/dotnet) branch HEAD +# For forward flow: compare against product repo branch HEAD +$freshnessRepo = if ($isForwardFlow) { $sourceRepo } else { "dotnet/dotnet" } +$freshnessRepoLabel = if ($isForwardFlow) { $sourceRepo } else { "VMR" } + if ($vmrCommit -and $vmrBranch) { - # Get current VMR branch HEAD (URL-encode branch name for path segments with /) + # Get current branch HEAD (URL-encode branch name for path segments with /) $encodedBranch = [uri]::EscapeDataString($vmrBranch) - $vmrHead = Invoke-GitHubApi "/repos/dotnet/dotnet/commits/$encodedBranch" - if ($vmrHead) { - $vmrHeadSha = $vmrHead.sha - $vmrHeadDate = $vmrHead.commit.committer.date + $branchHead = Invoke-GitHubApi "/repos/$freshnessRepo/commits/$encodedBranch" + if ($branchHead) { + $sourceHeadSha = $branchHead.sha + $sourceHeadDate = $branchHead.commit.committer.date Write-Status "PR snapshot" "$(Get-ShortSha $vmrCommit) (from PR body)" - Write-Status "VMR HEAD" "$(Get-ShortSha $vmrHeadSha) ($vmrHeadDate)" + Write-Status "$freshnessRepoLabel HEAD" "$(Get-ShortSha $sourceHeadSha) ($sourceHeadDate)" - if ($vmrCommit -eq $vmrHeadSha) { - Write-Host " ✅ PR is up to date with VMR branch" -ForegroundColor Green + if ($vmrCommit -eq $sourceHeadSha) { + Write-Host " ✅ PR is up to date with $freshnessRepoLabel branch" -ForegroundColor Green } else { - # Compare to find how many commits differ between the PR snapshot and the VMR - $compare = Invoke-GitHubApi "/repos/dotnet/dotnet/compare/$vmrCommit...$vmrHeadSha" + # Compare to find how many commits differ + $compare = Invoke-GitHubApi "/repos/$freshnessRepo/compare/$vmrCommit...$sourceHeadSha" if ($compare) { $aheadBy = $compare.ahead_by $behindBy = $compare.behind_by @@ -214,16 +235,16 @@ if ($vmrCommit -and $vmrBranch) { switch ($compareStatus) { 'ahead' { - Write-Host " ⚠️ VMR is $aheadBy commit(s) ahead of the PR snapshot" -ForegroundColor Yellow + Write-Host " ⚠️ $freshnessRepoLabel is $aheadBy commit(s) ahead of the PR snapshot" -ForegroundColor Yellow } 'behind' { - Write-Host " ⚠️ VMR is $behindBy commit(s) behind the PR snapshot" -ForegroundColor Yellow + Write-Host " ⚠️ $freshnessRepoLabel is $behindBy commit(s) behind the PR snapshot" -ForegroundColor Yellow } 'diverged' { - Write-Host " ⚠️ VMR and PR snapshot have diverged: VMR is $aheadBy commit(s) ahead and $behindBy commit(s) behind" -ForegroundColor Yellow + Write-Host " ⚠️ $freshnessRepoLabel and PR snapshot have diverged: $aheadBy commit(s) ahead and $behindBy commit(s) behind" -ForegroundColor Yellow } default { - Write-Host " ⚠️ VMR and PR snapshot differ (status: $compareStatus)" -ForegroundColor Yellow + Write-Host " ⚠️ $freshnessRepoLabel and PR snapshot differ (status: $compareStatus)" -ForegroundColor Yellow } } @@ -231,8 +252,8 @@ if ($vmrCommit -and $vmrBranch) { Write-Host "" $commitLabel = switch ($compareStatus) { 'ahead' { "Commits since PR snapshot:" } - 'behind' { "Commits in PR snapshot but not in VMR:" } - default { "Commits differing between VMR and PR snapshot:" } + 'behind' { "Commits in PR snapshot but not in $freshnessRepoLabel`:" } + default { "Commits differing:" } } Write-Host " $commitLabel" -ForegroundColor Yellow foreach ($c in $compare.commits) { @@ -263,7 +284,7 @@ if ($vmrCommit -and $vmrBranch) { } } else { - Write-Warning "Cannot check VMR freshness without VMR commit and branch info" + Write-Warning "Cannot check freshness without source commit and branch info" } # --- Step 4: Check staleness warnings (using comments from gh pr view) --- @@ -287,7 +308,8 @@ if ($pr.comments) { if ($stalenessWarnings.Count -gt 0) { Write-Host " ⚠️ Staleness warning detected ($($stalenessWarnings.Count) warning(s))" -ForegroundColor Yellow Write-Status "Latest warning" $lastStalenessComment.createdAt - Write-Host " The VMR received opposite codeflow (forward flow merged) while this PR was open." -ForegroundColor Yellow + $oppositeFlow = if ($isForwardFlow) { "backflow from VMR merged into $sourceRepo" } else { "forward flow merged into VMR" } + Write-Host " Opposite codeflow ($oppositeFlow) while this PR was open." -ForegroundColor Yellow Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow # Extract darc commands from the warning @@ -473,12 +495,12 @@ if ($stalenessWarnings.Count -gt 0) { $issues += "Staleness warning active — codeflow is blocked" } -if ($vmrCommit -and $vmrHeadSha -and $vmrCommit -ne $vmrHeadSha) { +if ($vmrCommit -and $sourceHeadSha -and $vmrCommit -ne $sourceHeadSha) { switch ($compareStatus) { - 'ahead' { $issues += "VMR is $aheadBy commit(s) ahead of PR snapshot" } - 'behind' { $issues += "VMR is $behindBy commit(s) behind PR snapshot" } - 'diverged' { $issues += "VMR and PR snapshot diverged ($aheadBy ahead, $behindBy behind)" } - default { $issues += "VMR and PR snapshot differ" } + 'ahead' { $issues += "$freshnessRepoLabel is $aheadBy commit(s) ahead of PR snapshot" } + 'behind' { $issues += "$freshnessRepoLabel is $behindBy commit(s) behind PR snapshot" } + 'diverged' { $issues += "$freshnessRepoLabel and PR snapshot diverged ($aheadBy ahead, $behindBy behind)" } + default { $issues += "$freshnessRepoLabel and PR snapshot differ" } } } From 6a97bd081cbded6a4334dcc9252a991f3029b399 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 16:55:39 -0600 Subject: [PATCH 06/19] Address review: remove dead code, add gh preflight, truncation warning, fix forward-flow manifest branch --- .../scripts/Get-CodeflowStatus.ps1 | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 92c9f22ffdbc07..04dcdde828e1e5 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -95,9 +95,14 @@ if ($repoParts.Count -ne 2) { # --- Step 1: Get PR details (single call for PR + comments + commits) --- Write-Section "Codeflow PR #$PRNumber in $Repository" +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Error "GitHub CLI (gh) is not installed or not in PATH. Install from https://cli.github.com/" + return +} + $prJson = gh pr view $PRNumber -R $Repository --json body,title,state,author,headRefName,baseRefName,createdAt,updatedAt,url,comments,commits 2>&1 if ($LASTEXITCODE -ne 0) { - Write-Error "Could not fetch PR #$PRNumber from $Repository" + Write-Error "Could not fetch PR #$PRNumber from $Repository. Ensure you are authenticated (gh auth login)." return } $pr = ($prJson -join "`n") | ConvertFrom-Json @@ -113,8 +118,6 @@ Write-Host " URL: $($pr.url)" $isMaestroPR = $pr.author.login -eq "dotnet-maestro[bot]" $isBackflow = $pr.title -match "Source code updates from dotnet/dotnet" $isForwardFlow = $pr.title -match "Source code updates from (dotnet/\S+)" -and -not $isBackflow -$flowDirection = if ($isBackflow) { "backflow" } elseif ($isForwardFlow) { "forward" } else { "unknown" } - if (-not $isMaestroPR -and -not $isBackflow -and -not $isForwardFlow) { Write-Warning "This does not appear to be a codeflow PR (author: $($pr.author.login), title: $($pr.title))" Write-Warning "Expected author 'dotnet-maestro[bot]' and title containing 'Source code updates from'" @@ -248,6 +251,13 @@ if ($vmrCommit -and $vmrBranch) { } } + if ($compare.total_commits -and $compare.commits) { + $returnedCommits = @($compare.commits).Count + if ($returnedCommits -lt $compare.total_commits) { + Write-Host " ⚠️ Compare API returned $returnedCommits of $($compare.total_commits) commits; listing may be incomplete." -ForegroundColor Yellow + } + } + if ($ShowCommits -and $compare.commits) { Write-Host "" $commitLabel = switch ($compareStatus) { @@ -401,11 +411,14 @@ if ($TraceFix) { } # Check if the fix is in the VMR source-manifest.json on the target branch - if ($fixIsMerged -and $vmrBranch) { + # For forward flow, the VMR target is the PR base branch; for backflow, use $vmrBranch + $vmrManifestBranch = if ($isForwardFlow -and $pr.baseRefName) { $pr.baseRefName } else { $vmrBranch } + if ($fixIsMerged -and $vmrManifestBranch) { Write-Host "" - Write-Host " Checking VMR source-manifest.json on $vmrBranch..." -ForegroundColor White + Write-Host " Checking VMR source-manifest.json on $vmrManifestBranch..." -ForegroundColor White - $manifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$vmrBranch" + $encodedManifestBranch = [uri]::EscapeDataString($vmrManifestBranch) + $manifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$encodedManifestBranch" $manifestJson = Invoke-GitHubApi $manifestUrl -Raw if ($manifestJson) { $manifest = $manifestJson | ConvertFrom-Json From 376b625c4b91998d0c50b4deb2c193939eb0737b Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 17:00:21 -0600 Subject: [PATCH 07/19] Fix forward-flow TraceFix snapshot ref, stderr capture, maestro login detection, branch inference --- .../scripts/Get-CodeflowStatus.ps1 | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 04dcdde828e1e5..e4b4ca9d67cfbb 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -100,7 +100,7 @@ if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { return } -$prJson = gh pr view $PRNumber -R $Repository --json body,title,state,author,headRefName,baseRefName,createdAt,updatedAt,url,comments,commits 2>&1 +$prJson = gh pr view $PRNumber -R $Repository --json body,title,state,author,headRefName,baseRefName,createdAt,updatedAt,url,comments,commits if ($LASTEXITCODE -ne 0) { Write-Error "Could not fetch PR #$PRNumber from $Repository. Ensure you are authenticated (gh auth login)." return @@ -195,9 +195,16 @@ if ($repoChanges.Count -gt 0) { if (-not $vmrCommit -or -not $vmrBranch) { Write-Warning "Could not parse VMR metadata from PR body. This may not be a codeflow PR." if (-not $vmrBranch) { - # Try to infer from the PR target branch - $vmrBranch = $pr.baseRefName - Write-Status "Inferred VMR Branch" "$vmrBranch (from PR target)" + # For backflow: infer from PR target (which is the product repo branch = VMR branch name) + # For forward flow: infer from PR head branch pattern or source repo context + if ($isForwardFlow) { + $vmrBranch = $pr.headRefName -replace '^darc-', '' -replace '-[a-f0-9-]+$', '' + if (-not $vmrBranch) { $vmrBranch = $pr.baseRefName } + } + else { + $vmrBranch = $pr.baseRefName + } + Write-Status "Inferred Branch" "$vmrBranch (from PR metadata)" } } @@ -341,7 +348,6 @@ else { # --- Step 5: Analyze PR branch commits (using commits from gh pr view) --- Write-Section "PR Branch Analysis" -$manualCommits = @() $prCommits = $pr.commits if ($prCommits) { $maestroCommits = @() @@ -350,12 +356,14 @@ if ($prCommits) { foreach ($c in $prCommits) { $msg = $c.messageHeadline - $author = if ($c.authors -and $c.authors.Count -gt 0) { $c.authors[0].name } else { "unknown" } + $authorLogin = if ($c.authors -and $c.authors.Count -gt 0) { $c.authors[0].login } else { $null } + $authorName = if ($c.authors -and $c.authors.Count -gt 0) { $c.authors[0].name } else { "unknown" } + $author = if ($authorLogin) { $authorLogin } else { $authorName } if ($msg -match "^Merge branch") { $mergeCommits += $c } - elseif ($author -eq "dotnet-maestro[bot]" -or $msg -eq "Update dependencies") { + elseif ($author -in @("dotnet-maestro[bot]", "dotnet-maestro") -or $msg -eq "Update dependencies") { $maestroCommits += $c } else { @@ -454,11 +462,17 @@ if ($TraceFix) { } # Now check if the PR's VMR snapshot includes this - if ($vmrCommit) { + # For backflow: $vmrCommit is a VMR SHA, use it directly + # For forward flow: $vmrCommit is a source repo SHA, use PR head commit in dotnet/dotnet instead + $snapshotRef = $vmrCommit + if ($isForwardFlow -and $pr.commits -and $pr.commits.Count -gt 0) { + $snapshotRef = $pr.commits[-1].oid + } + if ($snapshotRef) { Write-Host "" - Write-Host " Checking if fix is in the PR's VMR snapshot..." -ForegroundColor White + Write-Host " Checking if fix is in the PR's snapshot..." -ForegroundColor White - $snapshotManifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$vmrCommit" + $snapshotManifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$snapshotRef" $snapshotJson = Invoke-GitHubApi $snapshotManifestUrl -Raw if ($snapshotJson) { $snapshotData = $snapshotJson | ConvertFrom-Json From 155e8c0049943712f9351746e4216e7b78d0870c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 17:10:41 -0600 Subject: [PATCH 08/19] Add try/catch for manifest JSON parsing, use Get-ShortSha consistently --- .../scripts/Get-CodeflowStatus.ps1 | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index e4b4ca9d67cfbb..bea8e7470f6319 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -277,7 +277,7 @@ if ($vmrCommit -and $vmrBranch) { $msg = ($c.commit.message -split "`n")[0] if ($msg.Length -gt 100) { $msg = $msg.Substring(0, 97) + "..." } $date = $c.commit.committer.date - Write-Host " $($c.sha.Substring(0,8)) $date $msg" + Write-Host " $(Get-ShortSha $c.sha 8) $date $msg" } } @@ -429,7 +429,13 @@ if ($TraceFix) { $manifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$encodedManifestBranch" $manifestJson = Invoke-GitHubApi $manifestUrl -Raw if ($manifestJson) { - $manifest = $manifestJson | ConvertFrom-Json + try { + $manifest = $manifestJson | ConvertFrom-Json + } + catch { + Write-Warning "Could not parse VMR source-manifest.json: $_" + $manifest = $null + } # Find the repo in the manifest $escapedRepo = [regex]::Escape($traceRepo) @@ -475,7 +481,13 @@ if ($TraceFix) { $snapshotManifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$snapshotRef" $snapshotJson = Invoke-GitHubApi $snapshotManifestUrl -Raw if ($snapshotJson) { - $snapshotData = $snapshotJson | ConvertFrom-Json + try { + $snapshotData = $snapshotJson | ConvertFrom-Json + } + catch { + Write-Warning "Could not parse snapshot manifest: $_" + $snapshotData = $null + } $snapshotEntry = $snapshotData.repositories | Where-Object { $_.remoteUri -match "${escapedRepo}(\.git)?$" -or $_.path -eq $traceRepo From 1011269b6bb3b54eae64c2358f2873174922e25f Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 17:19:34 -0600 Subject: [PATCH 09/19] Add -CheckMissing mode to detect expected but missing backflow PRs --- .github/skills/vmr-codeflow-status/SKILL.md | 11 +- .../scripts/Get-CodeflowStatus.ps1 | 193 +++++++++++++++++- 2 files changed, 200 insertions(+), 4 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 7eb104b4f5b4bb..70a3d7f3982adb 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -27,16 +27,24 @@ 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 +./scripts/Get-CodeflowStatus.ps1 -Repository "dotnet/roslyn" -CheckMissing + +# Check a specific branch only +./scripts/Get-CodeflowStatus.ps1 -Repository "dotnet/sdk" -CheckMissing -Branch "main" ``` ## Key Parameters | Parameter | Description | |-----------|-------------| -| `-PRNumber` | GitHub PR number to analyze (required) | +| `-PRNumber` | GitHub PR number to analyze (required unless `-CheckMissing`) | | `-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 | +| `-Branch` | With `-CheckMissing`, only check a specific branch | ## What the Script Does @@ -46,6 +54,7 @@ Use this skill when: 4. **Analyzes PR commits** — Categorizes as auto-updates vs manual commits 5. **Traces fixes** (optional) — Checks if a specific fix has flowed through VMR → codeflow PR 6. **Recommends actions** — Suggests force trigger, close/reopen, merge as-is, or wait +7. **Checks for missing backflow** (optional) — Finds branches where a backflow PR should exist but doesn't ## 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 bea8e7470f6319..6856b52b0929fa 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -7,8 +7,10 @@ detects staleness warnings, traces specific fixes through the pipeline, and provides actionable recommendations. + Can also check if a backflow PR is expected but missing for a given repo/branch. + .PARAMETER PRNumber - GitHub PR number to analyze. + GitHub PR number to analyze. Required unless -CheckMissing is used. .PARAMETER Repository Target repository (default: dotnet/sdk). Format: owner/repo. @@ -20,22 +22,39 @@ .PARAMETER ShowCommits Show individual VMR commits between the PR snapshot and current branch HEAD. +.PARAMETER CheckMissing + Check if backflow PRs are expected but missing for a repository. When used, + PRNumber is not required. Finds the most recent merged backflow PR for each branch, + extracts its VMR commit, and compares against current VMR branch HEAD. + +.PARAMETER Branch + Optional. When used with -CheckMissing, only check a specific branch instead of all. + .EXAMPLE ./Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" .EXAMPLE ./Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" -TraceFix "dotnet/runtime#123974" + +.EXAMPLE + ./Get-CodeflowStatus.ps1 -Repository "dotnet/roslyn" -CheckMissing + +.EXAMPLE + ./Get-CodeflowStatus.ps1 -Repository "dotnet/roslyn" -CheckMissing -Branch "main" #> param( - [Parameter(Mandatory = $true)] [int]$PRNumber, [string]$Repository = "dotnet/sdk", [string]$TraceFix, - [switch]$ShowCommits + [switch]$ShowCommits, + + [switch]$CheckMissing, + + [string]$Branch ) $ErrorActionPreference = "Stop" @@ -92,6 +111,174 @@ if ($repoParts.Count -ne 2) { return } +# --- CheckMissing mode: find expected but missing backflow PRs --- +if ($CheckMissing) { + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Error "GitHub CLI (gh) is not installed or not in PATH. Install from https://cli.github.com/" + return + } + + Write-Section "Checking for missing backflow PRs in $Repository" + + # Find open backflow PRs (to know which branches are already covered) + $openPRsJson = gh search prs --repo $Repository --author "dotnet-maestro[bot]" --state open "Source code updates from dotnet/dotnet" --json number,title --limit 50 2>&1 + $openPRs = @() + if ($LASTEXITCODE -eq 0 -and $openPRsJson) { + $openPRs = ($openPRsJson -join "`n") | ConvertFrom-Json + } + $openBranches = @{} + foreach ($opr in $openPRs) { + if ($opr.title -match '^\[([^\]]+)\]') { + $openBranches[$Matches[1]] = $opr.number + } + } + + if ($openPRs.Count -gt 0) { + Write-Host " Open backflow PRs already exist:" -ForegroundColor White + foreach ($opr in $openPRs) { + Write-Host " #$($opr.number): $($opr.title)" -ForegroundColor Green + } + Write-Host "" + } + + # Find recently merged backflow PRs to discover branches and VMR commit mapping + $mergedPRsJson = gh search prs --repo $Repository --author "dotnet-maestro[bot]" --state closed --merged "Source code updates from dotnet/dotnet" --limit 30 --sort updated --json number,title,closedAt 2>&1 + $mergedPRs = @() + if ($LASTEXITCODE -eq 0 -and $mergedPRsJson) { + $mergedPRs = ($mergedPRsJson -join "`n") | ConvertFrom-Json + } + + if ($mergedPRs.Count -eq 0 -and $openPRs.Count -eq 0) { + Write-Host " No backflow PRs found (open or recently merged). This repo may not have backflow subscriptions." -ForegroundColor Yellow + return + } + + # Group merged PRs by branch, keeping only the most recent per branch + $branchLastMerged = @{} + foreach ($mpr in $mergedPRs) { + if ($mpr.title -match '^\[([^\]]+)\]') { + $branchName = $Matches[1] + if ($Branch -and $branchName -ne $Branch) { continue } + if (-not $branchLastMerged.ContainsKey($branchName)) { + $branchLastMerged[$branchName] = $mpr + } + } + } + + if ($Branch -and -not $branchLastMerged.ContainsKey($Branch) -and -not $openBranches.ContainsKey($Branch)) { + Write-Host " No backflow PRs found for branch '$Branch'." -ForegroundColor Yellow + return + } + + # For each branch without an open PR, check if VMR has moved past the last merged commit + $missingCount = 0 + $coveredCount = 0 + $upToDateCount = 0 + + foreach ($branchName in ($branchLastMerged.Keys | Sort-Object)) { + $lastPR = $branchLastMerged[$branchName] + Write-Host "" + Write-Host " Branch: $branchName" -ForegroundColor White + + if ($openBranches.ContainsKey($branchName)) { + Write-Host " ✅ Open backflow PR #$($openBranches[$branchName]) exists" -ForegroundColor Green + $coveredCount++ + continue + } + + # Get the PR body to extract VMR commit and VMR branch + $prDetailJson = gh pr view $lastPR.number -R $Repository --json body 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host " ⚠️ Could not fetch PR #$($lastPR.number) details" -ForegroundColor Yellow + continue + } + $prDetail = ($prDetailJson -join "`n") | ConvertFrom-Json + + $vmrCommitFromPR = $null + $vmrBranchFromPR = $null + if ($prDetail.body -match '\*\*Commit\*\*:\s*\[([a-f0-9]+)\]') { + $vmrCommitFromPR = $Matches[1] + } + if ($prDetail.body -match '\*\*Branch\*\*:\s*\[([^\]]+)\]') { + $vmrBranchFromPR = $Matches[1] + } + + if (-not $vmrCommitFromPR -or -not $vmrBranchFromPR) { + Write-Host " ⚠️ Could not parse VMR metadata from last merged PR #$($lastPR.number)" -ForegroundColor Yellow + continue + } + + Write-Host " Last merged: PR #$($lastPR.number) on $($lastPR.closedAt)" -ForegroundColor DarkGray + Write-Host " VMR branch: $vmrBranchFromPR" -ForegroundColor DarkGray + Write-Host " VMR commit: $(Get-ShortSha $vmrCommitFromPR)" -ForegroundColor DarkGray + + # Get current VMR branch HEAD + $encodedVmrBranch = [uri]::EscapeDataString($vmrBranchFromPR) + $vmrHead = Invoke-GitHubApi "/repos/dotnet/dotnet/commits/$encodedVmrBranch" + if (-not $vmrHead) { + Write-Host " ⚠️ Could not fetch VMR branch HEAD for $vmrBranchFromPR" -ForegroundColor Yellow + continue + } + + $vmrHeadSha = $vmrHead.sha + $vmrHeadDate = $vmrHead.commit.committer.date + + if ($vmrCommitFromPR -eq $vmrHeadSha) { + Write-Host " ✅ VMR branch is at same commit — no backflow needed" -ForegroundColor Green + $upToDateCount++ + } + else { + # Check how far ahead + $compare = Invoke-GitHubApi "/repos/dotnet/dotnet/compare/$vmrCommitFromPR...$vmrHeadSha" + $ahead = if ($compare) { $compare.ahead_by } else { "?" } + + Write-Host " 🔴 MISSING BACKFLOW PR" -ForegroundColor Red + Write-Host " VMR is $ahead commit(s) ahead since last merged PR" -ForegroundColor Yellow + Write-Host " VMR HEAD: $(Get-ShortSha $vmrHeadSha) ($vmrHeadDate)" -ForegroundColor DarkGray + Write-Host " Last merged VMR commit: $(Get-ShortSha $vmrCommitFromPR)" -ForegroundColor DarkGray + + # Check how long ago the last PR merged + $mergedTime = [DateTime]::Parse($lastPR.closedAt) + $elapsed = [DateTime]::UtcNow - $mergedTime + if ($elapsed.TotalHours -gt 6) { + Write-Host " ⚠️ Last PR merged $([math]::Round($elapsed.TotalHours, 1)) hours ago — Maestro may be stuck" -ForegroundColor Yellow + } + else { + Write-Host " ℹ️ Last PR merged $([math]::Round($elapsed.TotalHours, 1)) hours ago — Maestro may still be processing" -ForegroundColor DarkGray + } + $missingCount++ + } + } + + # Also check open-only branches (that weren't in merged list) + foreach ($branchName in ($openBranches.Keys | Sort-Object)) { + if (-not $branchLastMerged.ContainsKey($branchName)) { + if ($Branch -and $branchName -ne $Branch) { continue } + Write-Host "" + Write-Host " Branch: $branchName" -ForegroundColor White + Write-Host " ✅ Open backflow PR #$($openBranches[$branchName]) exists" -ForegroundColor Green + $coveredCount++ + } + } + + 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 + if ($missingCount -gt 0) { + Write-Host " Branches MISSING backflow PRs: $missingCount" -ForegroundColor Red + } + else { + Write-Host " No missing backflow PRs detected ✅" -ForegroundColor Green + } + return +} + +# --- Validate PRNumber for non-CheckMissing mode --- +if (-not $PRNumber) { + Write-Error "PRNumber is required unless -CheckMissing is used." + return +} + # --- Step 1: Get PR details (single call for PR + comments + commits) --- Write-Section "Codeflow PR #$PRNumber in $Repository" From 2b2fb20119a641a03dc3f1d9f3ecb5459401d447 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 17:23:13 -0600 Subject: [PATCH 10/19] Show pending forward flow PRs that would close the freshness gap for backflow PRs --- .../scripts/Get-CodeflowStatus.ps1 | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 6856b52b0929fa..e85b528e98d87f 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -483,6 +483,54 @@ if ($vmrCommit -and $vmrBranch) { Write-Host "" Write-Host " Missing updates from: $($uniqueRepos -join ', ')" -ForegroundColor Yellow } + + # --- For backflow PRs that are behind: check pending forward flow PRs --- + if ($isBackflow -and $compareStatus -eq 'ahead' -and $aheadBy -gt 0 -and $vmrBranch) { + $encodedVmrBranch = [uri]::EscapeDataString($vmrBranch) + $forwardPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from" --base $vmrBranch --json number,title --limit 20 2>&1 + $pendingForwardPRs = @() + if ($LASTEXITCODE -eq 0 -and $forwardPRsJson) { + $allForward = ($forwardPRsJson -join "`n") | ConvertFrom-Json + # Filter to forward flow PRs (not backflow) targeting this VMR branch + $pendingForwardPRs = $allForward | Where-Object { + $_.title -match "Source code updates from (dotnet/\S+)" -and + $Matches[1] -ne "dotnet/dotnet" + } + } + + if ($pendingForwardPRs.Count -gt 0) { + Write-Host "" + Write-Host " Pending forward flow PRs into VMR ($vmrBranch):" -ForegroundColor Cyan + + $coveredRepos = @() + foreach ($fpr in $pendingForwardPRs) { + $fprSourceRepo = $null + if ($fpr.title -match "Source code updates from (dotnet/\S+)") { + $fprSourceRepo = $Matches[1] + } + $coveredLabel = "" + if ($fprSourceRepo -and $uniqueRepos -contains $fprSourceRepo) { + $coveredRepos += $fprSourceRepo + $coveredLabel = " ← covers missing updates" + } + Write-Host " dotnet/dotnet#$($fpr.number): $($fpr.title)$coveredLabel" -ForegroundColor DarkGray + } + + if ($coveredRepos.Count -gt 0) { + $uncoveredRepos = $uniqueRepos | Where-Object { $_ -notin $coveredRepos } + $coveredCount = $coveredRepos.Count + $totalMissing = $uniqueRepos.Count + Write-Host "" + Write-Host " 📊 Forward flow coverage: $coveredCount of $totalMissing missing repo(s) have pending forward flow PRs" -ForegroundColor Cyan + if ($uncoveredRepos.Count -gt 0) { + Write-Host " Still waiting on: $($uncoveredRepos -join ', ')" -ForegroundColor Yellow + } + else { + Write-Host " ✅ All missing repos have pending forward flow — gap should close once they merge + new backflow triggers" -ForegroundColor Green + } + } + } + } } } } From 851e70eaf7813653008d5be76ca3730949d1e92d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 17:57:05 -0600 Subject: [PATCH 11/19] Add snapshot validation and conflict detection from session a906c259 --- .../scripts/Get-CodeflowStatus.ps1 | 191 +++++++++++++++--- 1 file changed, 165 insertions(+), 26 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index e85b528e98d87f..17a2abf7384168 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -395,6 +395,77 @@ if (-not $vmrCommit -or -not $vmrBranch) { } } +# For backflow: compare against VMR (dotnet/dotnet) branch HEAD +# For forward flow: compare against product repo branch HEAD +$freshnessRepo = if ($isForwardFlow) { $sourceRepo } else { "dotnet/dotnet" } +$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 --- +$branchVmrCommit = $null +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-f0-9]+)') { + $branchVmrCommit = $Matches[1] + break + } + } +} + +if ($branchVmrCommit -or $vmrCommit) { + Write-Section "Snapshot Validation" + 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 + } + 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 + } + else { + Write-Host " ⚠️ Could not resolve branch commit SHA $branchVmrCommit — falling back to PR body" -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 + } + } + elseif ($vmrCommit -and -not $branchVmrCommit) { + $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 + } + } + else { + Write-Host " ⚠️ No VMR SHA found in $commitCount branch commit messages — trusting PR body ($(Get-ShortSha $vmrCommit))" -ForegroundColor Yellow + } + } +} + # --- Step 3: Check source freshness --- $freshnessLabel = if ($isForwardFlow) { "Source Freshness" } else { "VMR Freshness" } Write-Section $freshnessLabel @@ -404,11 +475,6 @@ $aheadBy = 0 $behindBy = 0 $compareStatus = $null -# For backflow: compare against VMR (dotnet/dotnet) branch HEAD -# For forward flow: compare against product repo branch HEAD -$freshnessRepo = if ($isForwardFlow) { $sourceRepo } else { "dotnet/dotnet" } -$freshnessRepoLabel = if ($isForwardFlow) { $sourceRepo } else { "VMR" } - if ($vmrCommit -and $vmrBranch) { # Get current branch HEAD (URL-encode branch name for path segments with /) $encodedBranch = [uri]::EscapeDataString($vmrBranch) @@ -416,7 +482,12 @@ if ($vmrCommit -and $vmrBranch) { if ($branchHead) { $sourceHeadSha = $branchHead.sha $sourceHeadDate = $branchHead.commit.committer.date - Write-Status "PR snapshot" "$(Get-ShortSha $vmrCommit) (from PR body)" + $snapshotSource = if ($branchVmrCommit -and -not ($vmrCommit.StartsWith($branchVmrCommit) -or $branchVmrCommit.StartsWith($vmrCommit))) { + "from branch commit" + } else { + "from PR body" + } + Write-Status "PR snapshot" "$(Get-ShortSha $vmrCommit) ($snapshotSource)" Write-Status "$freshnessRepoLabel HEAD" "$(Get-ShortSha $sourceHeadSha) ($sourceHeadDate)" if ($vmrCommit -eq $sourceHeadSha) { @@ -539,8 +610,8 @@ else { Write-Warning "Cannot check freshness without source commit and branch info" } -# --- Step 4: Check staleness warnings (using comments from gh pr view) --- -Write-Section "Staleness Check" +# --- Step 4: Check staleness and conflict warnings (using comments from gh pr view) --- +Write-Section "Staleness & Conflict Check" $stalenessWarnings = @() $lastStalenessComment = $null @@ -557,33 +628,84 @@ if ($pr.comments) { } } -if ($stalenessWarnings.Count -gt 0) { - Write-Host " ⚠️ Staleness warning detected ($($stalenessWarnings.Count) warning(s))" -ForegroundColor Yellow - Write-Status "Latest warning" $lastStalenessComment.createdAt - $oppositeFlow = if ($isForwardFlow) { "backflow from VMR merged into $sourceRepo" } else { "forward flow merged into VMR" } - Write-Host " Opposite codeflow ($oppositeFlow) while this PR was open." -ForegroundColor Yellow - Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow - - # Extract darc commands from the warning - if ($lastStalenessComment.body -match 'darc trigger-subscriptions --id ([a-f0-9-]+)(?:\s+--force)?') { - Write-Host "" - Write-Host " Suggested commands from Maestro:" -ForegroundColor White - if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-f0-9-]+)\s*\n') { - Write-Host " Normal trigger: $($Matches[1])" +$conflictWarnings = @() +$lastConflictComment = $null + +if ($pr.comments) { + foreach ($comment in $pr.comments) { + $commentAuthor = $comment.author.login + if ($commentAuthor -eq "dotnet-maestro[bot]" -or $commentAuthor -eq "dotnet-maestro") { + if ($comment.body -match "Conflict detected") { + $conflictWarnings += $comment + $lastConflictComment = $comment + } + } + } +} + +if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { + if ($conflictWarnings.Count -gt 0) { + Write-Host " 🔴 Conflict detected ($($conflictWarnings.Count) conflict warning(s))" -ForegroundColor Red + Write-Status "Latest conflict" $lastConflictComment.createdAt + + # Extract conflicting files + $conflictFiles = @() + $fileMatches = [regex]::Matches($lastConflictComment.body, '-\s+`([^`]+)`\s*\n') + foreach ($fm in $fileMatches) { + $conflictFiles += $fm.Groups[1].Value + } + if ($conflictFiles.Count -gt 0) { + Write-Host " Conflicting files:" -ForegroundColor Yellow + foreach ($f in $conflictFiles) { + Write-Host " - $f" -ForegroundColor Yellow + } + } + + # Extract VMR commit from the conflict comment + if ($lastConflictComment.body -match 'sources from \[`([a-f0-9]+)`\]') { + Write-Host " Conflicting VMR commit: $($Matches[1])" -ForegroundColor DarkGray } - if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-f0-9-]+ --force)') { - Write-Host " Force trigger: $($Matches[1])" + + # Extract resolve command + if ($lastConflictComment.body -match '(darc vmr resolve-conflict --subscription [a-f0-9-]+)') { + Write-Host "" + Write-Host " Resolve command:" -ForegroundColor White + Write-Host " $($Matches[1])" -ForegroundColor DarkGray + } + } + + if ($stalenessWarnings.Count -gt 0) { + if ($conflictWarnings.Count -gt 0) { Write-Host "" } + Write-Host " ⚠️ Staleness warning detected ($($stalenessWarnings.Count) warning(s))" -ForegroundColor Yellow + Write-Status "Latest warning" $lastStalenessComment.createdAt + $oppositeFlow = if ($isForwardFlow) { "backflow from VMR merged into $sourceRepo" } else { "forward flow merged into VMR" } + Write-Host " Opposite codeflow ($oppositeFlow) while this PR was open." -ForegroundColor Yellow + Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow + + # Extract darc commands from the warning + if ($lastStalenessComment.body -match 'darc trigger-subscriptions --id ([a-f0-9-]+)(?:\s+--force)?') { + Write-Host "" + Write-Host " Suggested commands from Maestro:" -ForegroundColor White + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-f0-9-]+)\s*\n') { + Write-Host " Normal trigger: $($Matches[1])" + } + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-f0-9-]+ --force)') { + Write-Host " Force trigger: $($Matches[1])" + } } } + + if ($conflictWarnings.Count -eq 0 -and $stalenessWarnings.Count -eq 0) { + Write-Host " ✅ No staleness or conflict warnings found" -ForegroundColor Green + } } else { - Write-Host " ✅ No staleness warnings found" -ForegroundColor Green + Write-Host " ✅ No staleness or conflict warnings found" -ForegroundColor Green } # --- Step 5: Analyze PR branch commits (using commits from gh pr view) --- Write-Section "PR Branch Analysis" -$prCommits = $pr.commits if ($prCommits) { $maestroCommits = @() $manualCommits = @() @@ -765,6 +887,16 @@ Write-Section "Recommendations" $issues = @() # Summarize issues +if ($conflictWarnings.Count -gt 0) { + $conflictFileList = @() + if ($lastConflictComment) { + $fileMatches2 = [regex]::Matches($lastConflictComment.body, '-\s+`([^`]+)`\s*\n') + foreach ($fm in $fileMatches2) { $conflictFileList += $fm.Groups[1].Value } + } + $fileHint = if ($conflictFileList.Count -gt 0) { " in $($conflictFileList -join ', ')" } else { "" } + $issues += "Conflict detected$fileHint — manual resolution required" +} + if ($stalenessWarnings.Count -gt 0) { $issues += "Staleness warning active — codeflow is blocked" } @@ -797,7 +929,14 @@ else { Write-Host "" Write-Host " Options:" -ForegroundColor White - if ($stalenessWarnings.Count -gt 0 -and $manualCommits.Count -gt 0) { + if ($conflictWarnings.Count -gt 0) { + Write-Host " 1. Resolve conflicts — follow the darc vmr resolve-conflict instructions above" -ForegroundColor White + if ($subscriptionId) { + Write-Host " darc vmr resolve-conflict --subscription $subscriptionId" -ForegroundColor DarkGray + } + Write-Host " 2. Close & reopen — abandon this PR and let Maestro create a fresh one" -ForegroundColor White + } + elseif ($stalenessWarnings.Count -gt 0 -and $manualCommits.Count -gt 0) { Write-Host " 1. Merge as-is — keep manual commits, get remaining changes in next codeflow PR" -ForegroundColor White Write-Host " 2. Force trigger — updates codeflow but may revert manual commits" -ForegroundColor White if ($subscriptionId) { From 64dcaeed785fbea6cfbd554ea3451a84df427b16 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 17:59:41 -0600 Subject: [PATCH 12/19] Update SKILL.md with snapshot validation, conflict detection, forward flow coverage docs --- .github/skills/vmr-codeflow-status/SKILL.md | 31 ++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index 70a3d7f3982adb..c259965228a624 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -12,8 +12,9 @@ Analyze the health of VMR codeflow PRs (backflow from `dotnet/dotnet` to product Use this skill when: - A codeflow PR (from `dotnet-maestro[bot]`) has failing tests and you need to know if it's stale - 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") +- 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" ## Quick Start @@ -49,22 +50,31 @@ Use this skill when: ## What the Script Does 1. **Parses PR metadata** — Extracts VMR commit, subscription ID, build info from PR body -2. **Checks VMR freshness** — Compares PR's VMR snapshot against current VMR branch HEAD -3. **Detects staleness** — Finds Maestro "codeflow cannot continue" warnings -4. **Analyzes PR commits** — Categorizes as auto-updates vs manual commits -5. **Traces fixes** (optional) — Checks if a specific fix has flowed through VMR → codeflow PR -6. **Recommends actions** — Suggests force trigger, close/reopen, merge as-is, or wait -7. **Checks for missing backflow** (optional) — Finds branches where a backflow PR should exist but doesn't +2. **Validates snapshot** — Cross-references PR body commit against branch commit messages to detect stale metadata +3. **Checks VMR freshness** — Compares PR's VMR snapshot against current VMR branch HEAD +4. **Shows pending forward flow** — For behind backflow PRs, finds open forward flow PRs that would close part of the gap +5. **Detects staleness & conflicts** — Finds Maestro "codeflow cannot continue" warnings and "Conflict detected" messages with file lists and resolve commands +6. **Analyzes PR commits** — Categorizes as auto-updates vs manual commits +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 ## Interpreting Results ### Freshness - **✅ Up to date**: PR has the latest VMR snapshot - **⚠️ VMR is N commits ahead**: The PR is missing updates. Check if the missing commits contain the fix you need. +- **📊 Forward flow coverage**: Shows how many missing repos have pending forward flow PRs that would close part of the gap once merged. -### Staleness -- **✅ No staleness warnings**: Maestro can freely update the PR -- **⚠️ Staleness warning detected**: A forward flow merged while this backflow PR was open. Maestro blocked further updates. +### Snapshot Validation +- **✅ Match**: PR body commit matches the branch's actual "Backflow from" commit +- **⚠️ Mismatch**: PR body is stale — the script automatically uses the branch-derived commit for freshness checks +- **ℹ️ Initial commit only**: PR body can't be verified yet (no "Backflow from" commit exists) + +### Staleness & Conflicts +- **✅ No warnings**: Maestro can freely update the PR +- **⚠️ Staleness warning**: A forward flow merged while this backflow PR was open. Maestro blocked further updates. +- **🔴 Conflict detected**: Maestro found merge conflicts. Shows conflicting files and `darc vmr resolve-conflict` command. ### Manual Commits Manual commits on the PR branch are at risk if the PR is closed or force-triggered. The script lists them so you can decide whether to preserve them. @@ -93,6 +103,7 @@ darc get-subscriptions --target-repo dotnet/sdk --source-repo dotnet/dotnet darc get-build --id # Resolve codeflow conflicts locally +darc vmr resolve-conflict --subscription darc vmr resolve --subscription --build ``` From 1a54feaa0cf47d2b02fc651cecef73d094691ae1 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 18:03:18 -0600 Subject: [PATCH 13/19] Replace unused \ with regex validation --- .../skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 17a2abf7384168..5ef476b99f8def 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -105,8 +105,7 @@ function Write-Status { } # --- Parse repo owner/name --- -$repoParts = $Repository -split '/' -if ($repoParts.Count -ne 2) { +if ($Repository -notmatch '^[^/]+/[^/]+$') { Write-Error "Repository must be in format 'owner/repo' (e.g., 'dotnet/sdk')" return } From 4579f35cb702a8ef51f1a8f933299037b18328c9 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 18:05:52 -0600 Subject: [PATCH 14/19] Fix snapshot label bug, remove dead code, align darc resolve-conflict docs --- .../references/vmr-codeflow-reference.md | 2 +- .../scripts/Get-CodeflowStatus.ps1 | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md b/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md index 783fa5562edf57..cfbb5e40fb1f55 100644 --- a/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md +++ b/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md @@ -83,7 +83,7 @@ darc update-dependencies --subscription --dry-run ```bash # Resolve codeflow conflicts locally -darc vmr resolve --subscription --build +darc vmr resolve-conflict --subscription --build # Flow source from VMR → local repo darc vmr backflow --subscription diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 5ef476b99f8def..24d09cd557e1cb 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -424,6 +424,7 @@ if ($branchVmrCommit -or $vmrCommit) { if ($branchVmrCommit -and $vmrCommit) { $bodyShort = Get-ShortSha $vmrCommit $branchShort = $branchVmrCommit # already short from commit message + $usedBranchSnapshot = $false if ($vmrCommit.StartsWith($branchVmrCommit) -or $branchVmrCommit.StartsWith($vmrCommit)) { Write-Host " ✅ PR body snapshot ($bodyShort) matches branch commit ($branchShort)" -ForegroundColor Green } @@ -434,6 +435,7 @@ if ($branchVmrCommit -or $vmrCommit) { $resolvedCommit = Invoke-GitHubApi "/repos/$freshnessRepo/commits/$branchVmrCommit" if ($resolvedCommit) { $vmrCommit = $resolvedCommit.sha + $usedBranchSnapshot = $true } else { Write-Host " ⚠️ Could not resolve branch commit SHA $branchVmrCommit — falling back to PR body" -ForegroundColor Yellow @@ -441,11 +443,13 @@ if ($branchVmrCommit -or $vmrCommit) { } } elseif ($branchVmrCommit -and -not $vmrCommit) { + $usedBranchSnapshot = $false 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) { @@ -481,11 +485,7 @@ if ($vmrCommit -and $vmrBranch) { if ($branchHead) { $sourceHeadSha = $branchHead.sha $sourceHeadDate = $branchHead.commit.committer.date - $snapshotSource = if ($branchVmrCommit -and -not ($vmrCommit.StartsWith($branchVmrCommit) -or $branchVmrCommit.StartsWith($vmrCommit))) { - "from branch commit" - } else { - "from PR body" - } + $snapshotSource = if ($usedBranchSnapshot) { "from branch commit" } else { "from PR body" } Write-Status "PR snapshot" "$(Get-ShortSha $vmrCommit) ($snapshotSource)" Write-Status "$freshnessRepoLabel HEAD" "$(Get-ShortSha $sourceHeadSha) ($sourceHeadDate)" @@ -556,7 +556,6 @@ if ($vmrCommit -and $vmrBranch) { # --- For backflow PRs that are behind: check pending forward flow PRs --- if ($isBackflow -and $compareStatus -eq 'ahead' -and $aheadBy -gt 0 -and $vmrBranch) { - $encodedVmrBranch = [uri]::EscapeDataString($vmrBranch) $forwardPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from" --base $vmrBranch --json number,title --limit 20 2>&1 $pendingForwardPRs = @() if ($LASTEXITCODE -eq 0 -and $forwardPRsJson) { @@ -693,10 +692,6 @@ if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { } } } - - if ($conflictWarnings.Count -eq 0 -and $stalenessWarnings.Count -eq 0) { - Write-Host " ✅ No staleness or conflict warnings found" -ForegroundColor Green - } } else { Write-Host " ✅ No staleness or conflict warnings found" -ForegroundColor Green From 79eabf5d9701af53c84d18fe2c0ea67dfd921b53 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 18:11:26 -0600 Subject: [PATCH 15/19] Handle compare status 'identical', initialize usedBranchSnapshot consistently --- .../vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 24d09cd557e1cb..c95b208157360a 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -421,10 +421,10 @@ if ($prCommits) { 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 - $usedBranchSnapshot = $false if ($vmrCommit.StartsWith($branchVmrCommit) -or $branchVmrCommit.StartsWith($vmrCommit)) { Write-Host " ✅ PR body snapshot ($bodyShort) matches branch commit ($branchShort)" -ForegroundColor Green } @@ -443,7 +443,6 @@ if ($branchVmrCommit -or $vmrCommit) { } } elseif ($branchVmrCommit -and -not $vmrCommit) { - $usedBranchSnapshot = $false 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" @@ -501,6 +500,9 @@ if ($vmrCommit -and $vmrBranch) { $compareStatus = $compare.status switch ($compareStatus) { + 'identical' { + Write-Host " ✅ PR is up to date with $freshnessRepoLabel branch" -ForegroundColor Green + } 'ahead' { Write-Host " ⚠️ $freshnessRepoLabel is $aheadBy commit(s) ahead of the PR snapshot" -ForegroundColor Yellow } @@ -895,7 +897,7 @@ if ($stalenessWarnings.Count -gt 0) { $issues += "Staleness warning active — codeflow is blocked" } -if ($vmrCommit -and $sourceHeadSha -and $vmrCommit -ne $sourceHeadSha) { +if ($vmrCommit -and $sourceHeadSha -and $vmrCommit -ne $sourceHeadSha -and $compareStatus -ne 'identical') { switch ($compareStatus) { 'ahead' { $issues += "$freshnessRepoLabel is $aheadBy commit(s) ahead of PR snapshot" } 'behind' { $issues += "$freshnessRepoLabel is $behindBy commit(s) behind PR snapshot" } From 5e1544c273f0fe1256a30a0438e3426f16d4e47d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 18:18:54 -0600 Subject: [PATCH 16/19] Detect manual codeflow-like commits when flow is paused --- .../scripts/Get-CodeflowStatus.ps1 | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index c95b208157360a..36cbc8cabb17b5 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -739,6 +739,29 @@ if ($prCommits) { Write-Host " $(Get-ShortSha $c.oid 8) [$authorName] $msg" } } + + # Detect manual commits that look like codeflow-like changes (someone manually + # doing what Maestro would do while flow is paused) + $codeflowLikeManualCommits = @() + foreach ($c in $manualCommits) { + $msg = $c.messageHeadline + if ($msg -match 'Update dependencies' -or + $msg -match 'Version\.Details\.xml' -or + $msg -match 'Versions\.props' -or + $msg -match '[Bb]ackflow' -or + $msg -match '[Ff]orward flow' -or + $msg -match 'from dotnet/' -or + $msg -match '[a-f0-9]{7,40}' -or + $msg -match 'src/SourceBuild') { + $codeflowLikeManualCommits += $c + } + } + + if ($codeflowLikeManualCommits.Count -gt 0 -and $stalenessWarnings.Count -gt 0) { + Write-Host "" + Write-Host " ⚠️ $($codeflowLikeManualCommits.Count) manual commit(s) appear to contain codeflow-like changes while flow is paused" -ForegroundColor Yellow + Write-Host " The freshness gap reported above may be partially covered by these manual updates" -ForegroundColor DarkGray + } } # --- Step 6: Trace a specific fix (optional) --- @@ -933,6 +956,11 @@ else { Write-Host " 2. Close & reopen — abandon this PR and let Maestro create a fresh one" -ForegroundColor White } elseif ($stalenessWarnings.Count -gt 0 -and $manualCommits.Count -gt 0) { + if ($codeflowLikeManualCommits -and $codeflowLikeManualCommits.Count -gt 0) { + Write-Host " ℹ️ Note: Some manual commits appear to contain codeflow-like changes —" -ForegroundColor DarkGray + Write-Host " the reported freshness gap may already be partially addressed" -ForegroundColor DarkGray + Write-Host "" + } Write-Host " 1. Merge as-is — keep manual commits, get remaining changes in next codeflow PR" -ForegroundColor White Write-Host " 2. Force trigger — updates codeflow but may revert manual commits" -ForegroundColor White if ($subscriptionId) { From d3ec37fe343d9bc4b2941986ab67dd20e23915a2 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 18:21:32 -0600 Subject: [PATCH 17/19] Fix \r\n in regexes, case-insensitive hex, deduplicate conflict file extraction --- .../scripts/Get-CodeflowStatus.ps1 | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index 36cbc8cabb17b5..a848996dcc6024 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -365,7 +365,7 @@ if ($body -match '\*\*Commit Diff\*\*:\s*\[([^\]]+)\]\(([^\)]+)\)') { # Extract associated repo changes from footer $repoChanges = @() -$changeMatches = [regex]::Matches($body, '- (https://github\.com/([^/]+/[^/]+)/compare/([a-f0-9]+)\.\.\.([a-f0-9]+))') +$changeMatches = [regex]::Matches($body, '- (https://github\.com/([^/]+/[^/]+)/compare/([a-fA-F0-9]+)\.\.\.([a-fA-F0-9]+))') foreach ($m in $changeMatches) { $repoChanges += @{ URL = $m.Groups[1].Value @@ -412,8 +412,9 @@ if ($prCommits) { 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-f0-9]+)') { + 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) break } } @@ -650,7 +651,7 @@ if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { # Extract conflicting files $conflictFiles = @() - $fileMatches = [regex]::Matches($lastConflictComment.body, '-\s+`([^`]+)`\s*\n') + $fileMatches = [regex]::Matches($lastConflictComment.body, '-\s+`([^`]+)`\s*\r?\n') foreach ($fm in $fileMatches) { $conflictFiles += $fm.Groups[1].Value } @@ -662,12 +663,12 @@ if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { } # Extract VMR commit from the conflict comment - if ($lastConflictComment.body -match 'sources from \[`([a-f0-9]+)`\]') { + if ($lastConflictComment.body -match 'sources from \[`([a-fA-F0-9]+)`\]') { Write-Host " Conflicting VMR commit: $($Matches[1])" -ForegroundColor DarkGray } # Extract resolve command - if ($lastConflictComment.body -match '(darc vmr resolve-conflict --subscription [a-f0-9-]+)') { + if ($lastConflictComment.body -match '(darc vmr resolve-conflict --subscription [a-fA-F0-9-]+)') { Write-Host "" Write-Host " Resolve command:" -ForegroundColor White Write-Host " $($Matches[1])" -ForegroundColor DarkGray @@ -683,13 +684,13 @@ if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow # Extract darc commands from the warning - if ($lastStalenessComment.body -match 'darc trigger-subscriptions --id ([a-f0-9-]+)(?:\s+--force)?') { + if ($lastStalenessComment.body -match 'darc trigger-subscriptions --id ([a-fA-F0-9-]+)(?:\s+--force)?') { Write-Host "" Write-Host " Suggested commands from Maestro:" -ForegroundColor White - if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-f0-9-]+)\s*\n') { + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-fA-F0-9-]+)\s*\r?\n') { Write-Host " Normal trigger: $($Matches[1])" } - if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-f0-9-]+ --force)') { + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-fA-F0-9-]+ --force)') { Write-Host " Force trigger: $($Matches[1])" } } @@ -907,12 +908,7 @@ $issues = @() # Summarize issues if ($conflictWarnings.Count -gt 0) { - $conflictFileList = @() - if ($lastConflictComment) { - $fileMatches2 = [regex]::Matches($lastConflictComment.body, '-\s+`([^`]+)`\s*\n') - foreach ($fm in $fileMatches2) { $conflictFileList += $fm.Groups[1].Value } - } - $fileHint = if ($conflictFileList.Count -gt 0) { " in $($conflictFileList -join ', ')" } else { "" } + $fileHint = if ($conflictFiles -and $conflictFiles.Count -gt 0) { " in $($conflictFiles -join ', ')" } else { "" } $issues += "Conflict detected$fileHint — manual resolution required" } From 57559cd1a5baee58b70a6768da207a550efdcbc1 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 18:54:49 -0600 Subject: [PATCH 18/19] Fix UTC time parsing, capture full resolve-conflict command, case-insensitive commit SHA parsing, align SKILL.md commands --- .github/skills/vmr-codeflow-status/SKILL.md | 1 - .../vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md index c259965228a624..45c0eed81c3059 100644 --- a/.github/skills/vmr-codeflow-status/SKILL.md +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -104,7 +104,6 @@ darc get-build --id # Resolve codeflow conflicts locally darc vmr resolve-conflict --subscription -darc vmr resolve --subscription --build ``` Install darc via `eng\common\darc-init.ps1` in any arcade-enabled repository. diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index a848996dcc6024..f90e657010ba22 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -195,7 +195,7 @@ if ($CheckMissing) { $vmrCommitFromPR = $null $vmrBranchFromPR = $null - if ($prDetail.body -match '\*\*Commit\*\*:\s*\[([a-f0-9]+)\]') { + if ($prDetail.body -match '\*\*Commit\*\*:\s*\[([a-fA-F0-9]+)\]') { $vmrCommitFromPR = $Matches[1] } if ($prDetail.body -match '\*\*Branch\*\*:\s*\[([^\]]+)\]') { @@ -237,7 +237,7 @@ if ($CheckMissing) { Write-Host " Last merged VMR commit: $(Get-ShortSha $vmrCommitFromPR)" -ForegroundColor DarkGray # Check how long ago the last PR merged - $mergedTime = [DateTime]::Parse($lastPR.closedAt) + $mergedTime = [DateTimeOffset]::Parse($lastPR.closedAt).UtcDateTime $elapsed = [DateTime]::UtcNow - $mergedTime if ($elapsed.TotalHours -gt 6) { Write-Host " ⚠️ Last PR merged $([math]::Round($elapsed.TotalHours, 1)) hours ago — Maestro may be stuck" -ForegroundColor Yellow @@ -331,7 +331,7 @@ if ($body -match '\(Begin:([a-f0-9-]+)\)') { # Extract source commit (VMR commit for backflow, repo commit for forward flow) $sourceCommit = $null -if ($body -match '\*\*Commit\*\*:\s*\[([a-f0-9]+)\]') { +if ($body -match '\*\*Commit\*\*:\s*\[([a-fA-F0-9]+)\]') { $sourceCommit = $Matches[1] $commitLabel = if ($isForwardFlow) { "Source Commit" } else { "VMR Commit" } Write-Status $commitLabel $sourceCommit @@ -668,7 +668,7 @@ if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { } # Extract resolve command - if ($lastConflictComment.body -match '(darc vmr resolve-conflict --subscription [a-fA-F0-9-]+)') { + if ($lastConflictComment.body -match '(darc vmr resolve-conflict --subscription [a-fA-F0-9-]+(?:\s+--build [a-fA-F0-9-]+)?)') { Write-Host "" Write-Host " Resolve command:" -ForegroundColor White Write-Host " $($Matches[1])" -ForegroundColor DarkGray From f00d4b66030c6985faaab43a494b58083200a557 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 6 Feb 2026 20:15:01 -0600 Subject: [PATCH 19/19] Fix 2>&1 stderr corruption in gh search, prefix-match SHA comparison in CheckMissing --- .../scripts/Get-CodeflowStatus.ps1 | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 index f90e657010ba22..50fb720837ffca 100644 --- a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -120,10 +120,10 @@ if ($CheckMissing) { Write-Section "Checking for missing backflow PRs in $Repository" # Find open backflow PRs (to know which branches are already covered) - $openPRsJson = gh search prs --repo $Repository --author "dotnet-maestro[bot]" --state open "Source code updates from dotnet/dotnet" --json number,title --limit 50 2>&1 + $openPRsJson = gh search prs --repo $Repository --author "dotnet-maestro[bot]" --state open "Source code updates from dotnet/dotnet" --json number,title --limit 50 2>$null $openPRs = @() if ($LASTEXITCODE -eq 0 -and $openPRsJson) { - $openPRs = ($openPRsJson -join "`n") | ConvertFrom-Json + try { $openPRs = ($openPRsJson -join "`n") | ConvertFrom-Json } catch { $openPRs = @() } } $openBranches = @{} foreach ($opr in $openPRs) { @@ -141,10 +141,10 @@ if ($CheckMissing) { } # Find recently merged backflow PRs to discover branches and VMR commit mapping - $mergedPRsJson = gh search prs --repo $Repository --author "dotnet-maestro[bot]" --state closed --merged "Source code updates from dotnet/dotnet" --limit 30 --sort updated --json number,title,closedAt 2>&1 + $mergedPRsJson = gh search prs --repo $Repository --author "dotnet-maestro[bot]" --state closed --merged "Source code updates from dotnet/dotnet" --limit 30 --sort updated --json number,title,closedAt 2>$null $mergedPRs = @() if ($LASTEXITCODE -eq 0 -and $mergedPRsJson) { - $mergedPRs = ($mergedPRsJson -join "`n") | ConvertFrom-Json + try { $mergedPRs = ($mergedPRsJson -join "`n") | ConvertFrom-Json } catch { $mergedPRs = @() } } if ($mergedPRs.Count -eq 0 -and $openPRs.Count -eq 0) { @@ -222,7 +222,7 @@ if ($CheckMissing) { $vmrHeadSha = $vmrHead.sha $vmrHeadDate = $vmrHead.commit.committer.date - if ($vmrCommitFromPR -eq $vmrHeadSha) { + if ($vmrCommitFromPR -eq $vmrHeadSha -or $vmrHeadSha.StartsWith($vmrCommitFromPR) -or $vmrCommitFromPR.StartsWith($vmrHeadSha)) { Write-Host " ✅ VMR branch is at same commit — no backflow needed" -ForegroundColor Green $upToDateCount++ } @@ -559,14 +559,19 @@ if ($vmrCommit -and $vmrBranch) { # --- For backflow PRs that are behind: check pending forward flow PRs --- if ($isBackflow -and $compareStatus -eq 'ahead' -and $aheadBy -gt 0 -and $vmrBranch) { - $forwardPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from" --base $vmrBranch --json number,title --limit 20 2>&1 + $forwardPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from" --base $vmrBranch --json number,title --limit 20 2>$null $pendingForwardPRs = @() if ($LASTEXITCODE -eq 0 -and $forwardPRsJson) { - $allForward = ($forwardPRsJson -join "`n") | ConvertFrom-Json - # Filter to forward flow PRs (not backflow) targeting this VMR branch - $pendingForwardPRs = $allForward | Where-Object { - $_.title -match "Source code updates from (dotnet/\S+)" -and - $Matches[1] -ne "dotnet/dotnet" + try { + $allForward = ($forwardPRsJson -join "`n") | ConvertFrom-Json + # Filter to forward flow PRs (not backflow) targeting this VMR branch + $pendingForwardPRs = $allForward | Where-Object { + $_.title -match "Source code updates from (dotnet/\S+)" -and + $Matches[1] -ne "dotnet/dotnet" + } + } + catch { + Write-Warning "Failed to parse forward flow PR search results. Skipping forward flow analysis." } }