diff --git a/.github/prompts/branch-mirror-status.prompt.md b/.github/prompts/branch-mirror-status.prompt.md deleted file mode 100644 index 9d5751ebc36d..000000000000 --- a/.github/prompts/branch-mirror-status.prompt.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: "Check if public GitHub release branches have been mirrored to internal Azure DevOps repos for dotnet/sdk and dotnet/installer." ---- - -# Branch Mirror Status Check - -Check whether the latest commits on public GitHub release branches have been mirrored to the corresponding `internal/release/*` branches in Azure DevOps. - -## Branch Mappings - -| GitHub Repo | GitHub Branch | AzDo Project | AzDo Repo | AzDo Branch | -|---|---|---|---|---| -| dotnet/sdk | release/8.0.1xx | internal | dotnet-sdk | internal/release/8.0.1xx | -| dotnet/sdk | release/8.0.4xx | internal | dotnet-sdk | internal/release/8.0.4xx | -| dotnet/sdk | release/9.0.1xx | internal | dotnet-sdk | internal/release/9.0.1xx | -| dotnet/sdk | release/9.0.3xx | internal | dotnet-sdk | internal/release/9.0.3xx | -| dotnet/installer | release/8.0.1xx | internal | dotnet-installer | internal/release/8.0.1xx | -| dotnet/installer | release/8.0.4xx | internal | dotnet-installer | internal/release/8.0.4xx | - -## Procedure - -### Step 1: Get latest commits from GitHub - -For **each** row in the table above, call `github-mcp-server-list_commits` to get the most recent commit on the public branch. Make all calls **in parallel** for efficiency. - -Parameters for each call: -- `owner`: the GitHub org (e.g., `dotnet`) -- `repo`: the GitHub repo name (e.g., `sdk` or `installer`) -- `sha`: the branch name (e.g., `release/8.0.1xx`) -- `perPage`: 1 (we only need the latest commit) - -Record the **commit SHA** and **commit message** from each response. - -### Step 2: Check if each commit exists on the corresponding AzDo branch - -For **each** row, verify the GitHub commit SHA is present on the correct AzDo branch. Make all calls **in parallel**. - -The AzDo `search_commits` tool name varies by session — look for one matching `*repo_search_commits` that can access the `internal` project in the `dnceng` org. Common names include `dnceng-azure-devop-repo_search_commits` or `azure-devops-repo_search_commits`. - -#### Step 2a: Search for merge commits referencing the GitHub SHA - -Use `search_commits` with: -- `project`: `internal` -- `repository`: the AzDo repo name (e.g., `dotnet-sdk` or `dotnet-installer`) -- `version`: the AzDo branch name (e.g., `internal/release/8.0.1xx`) -- `versionType`: `Branch` -- `searchText`: the GitHub commit SHA from Step 1 -- `top`: 5 - -This searches the branch history for commits whose message contains the GitHub SHA. The mirroring service typically creates merge commits with messages like `Merge commit ''`, so a match confirms the commit was mirrored to the correct branch. - -If a match is found, the mirror is **up to date**. - -#### Step 2b: If no match, check for direct push (same SHA on branch) - -Sometimes commits are pushed directly to the AzDo branch without a merge commit wrapper, so the SHA is identical but doesn't appear in any commit message. If Step 2a returns no results, get the latest commits on the AzDo branch: - -Use `search_commits` with: -- `project`: `internal` -- `repository`: the AzDo repo name -- `version`: the AzDo branch name -- `versionType`: `Branch` -- `top`: 5 - -Then check if any returned commit's `commitId` exactly matches the GitHub SHA from Step 1. If it does, the mirror is **up to date** (direct push). If not, the mirror is **behind**. - -**Important:** Do **not** use `commitIds` alone — that searches the entire repo regardless of branch and does not confirm the commit is on the target branch. - -### Step 3: Present results - -Display a summary table: - -``` -| Repo | Branch | Latest GH Commit | Mirrored? | Details | -|---|---|---|---|---| -| dotnet/sdk | release/8.0.1xx | abc1234 "commit msg" | ✅ / ❌ | | -| ... | ... | ... | ... | | -``` - -For any ❌ entries, also check the latest commit on the AzDo branch to show how far behind it is: -- Use `search_commits` with `project`, `repository`, `version` (the AzDo branch name), and `top: 1` to get the most recent AzDo commit. -- Report the AzDo branch's latest commit SHA and date in the Details column. - -## Troubleshooting - -If the AzDo tools return errors about the project or repository not being found, the AzDo MCP connection may not be configured for the `dnceng` organization. In that case: -- Try other available `*repo_search_commits` or `*repo_list_repos_by_project` tools to find one that can access `project: internal`. -- If no tool can access the project, inform the user that the AzDo MCP connection may need to be configured for the `dnceng` organization. diff --git a/eng/Get-BranchMirrorStatus.ps1 b/eng/Get-BranchMirrorStatus.ps1 new file mode 100755 index 000000000000..099d64652140 --- /dev/null +++ b/eng/Get-BranchMirrorStatus.ps1 @@ -0,0 +1,212 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Check if public GitHub release branches have been mirrored to internal Azure DevOps repos. + +.DESCRIPTION + For each configured branch mapping (dotnet/sdk, dotnet/installer), fetches the latest + commit on the public GitHub branch and checks whether it has been mirrored to the + corresponding internal/release/* branch in Azure DevOps (dnceng org, internal project). + + Output is always plain markdown, safe for piping to a file or rendering. + +.PARAMETER SearchDepth + Number of AzDo commits to fetch when searching for the GitHub SHA. Default: 50. + +.EXAMPLE + ./eng/Get-BranchMirrorStatus.ps1 + ./eng/Get-BranchMirrorStatus.ps1 -SearchDepth 100 + ./eng/Get-BranchMirrorStatus.ps1 > status.md +#> + +[CmdletBinding()] +param( + [int]$SearchDepth = 50 +) + +$ErrorActionPreference = 'Continue' + +$AzDoOrg = "https://dev.azure.com/dnceng" +$AzDoResourceId = "499b84ac-1321-427f-aa17-267ca6975798" + +# Branch mappings for GitHub-to-Azure-DevOps mirror status checks. +$Mappings = @( + @{ GHOrg = 'dotnet'; GHRepo = 'sdk'; GHBranch = 'release/8.0.1xx'; AzDoProject = 'internal'; AzDoRepo = 'dotnet-sdk'; AzDoBranch = 'internal/release/8.0.1xx' } + @{ GHOrg = 'dotnet'; GHRepo = 'sdk'; GHBranch = 'release/8.0.4xx'; AzDoProject = 'internal'; AzDoRepo = 'dotnet-sdk'; AzDoBranch = 'internal/release/8.0.4xx' } + @{ GHOrg = 'dotnet'; GHRepo = 'sdk'; GHBranch = 'release/9.0.1xx'; AzDoProject = 'internal'; AzDoRepo = 'dotnet-sdk'; AzDoBranch = 'internal/release/9.0.1xx' } + @{ GHOrg = 'dotnet'; GHRepo = 'sdk'; GHBranch = 'release/9.0.3xx'; AzDoProject = 'internal'; AzDoRepo = 'dotnet-sdk'; AzDoBranch = 'internal/release/9.0.3xx' } + @{ GHOrg = 'dotnet'; GHRepo = 'installer'; GHBranch = 'release/8.0.1xx'; AzDoProject = 'internal'; AzDoRepo = 'dotnet-installer'; AzDoBranch = 'internal/release/8.0.1xx' } + @{ GHOrg = 'dotnet'; GHRepo = 'installer'; GHBranch = 'release/8.0.4xx'; AzDoProject = 'internal'; AzDoRepo = 'dotnet-installer'; AzDoBranch = 'internal/release/8.0.4xx' } +) + +function Format-TableCell { + param([string]$Text, [int]$MaxLength = 80) + + if ([string]::IsNullOrEmpty($Text)) { return '' } + $Text = $Text -replace '\|', '\\|' + $Text = $Text -replace '\r?\n', ' ' + if ($Text.Length -gt $MaxLength) { + $Text = $Text.Substring(0, $MaxLength - 3) + '...' + } + return $Text +} + +function Test-Prerequisites { + $errors = @() + + # Check gh CLI + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + $errors += "``gh`` CLI is not installed. Install from https://cli.github.com/" + } + else { + $null = gh auth status 2>&1 + if ($LASTEXITCODE -ne 0) { + $errors += "``gh`` CLI is not authenticated. Run ``gh auth login``." + } + } + + # Check az CLI + if (-not (Get-Command az -ErrorAction SilentlyContinue)) { + $errors += "``az`` CLI is not installed. Install from https://aka.ms/install-az-cli" + } + else { + # Probe actual AzDo access (not just az login) + $probeUrl = "$AzDoOrg/internal/_apis/git/repositories?api-version=7.1&`$top=1" + $null = az rest --method get --url $probeUrl --resource $AzDoResourceId --only-show-errors 2>&1 + if ($LASTEXITCODE -ne 0) { + $errors += "Cannot access Azure DevOps (dnceng/internal). Run ``az login`` and ensure you have access to the dnceng organization." + } + } + + if ($errors.Count -gt 0) { + Write-Output "## Prerequisites Failed" + Write-Output "" + foreach ($e in $errors) { + Write-Output "- $e" + } + exit 1 + } +} + +function Get-GitHubLatestCommit { + param( + [string]$Org, + [string]$Repo, + [string]$Branch + ) + + $rawJson = gh api "repos/$Org/$Repo/commits?sha=$Branch&per_page=1" 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "GitHub API error for $Org/$Repo ($Branch): $rawJson" + } + $commits = @($rawJson | ConvertFrom-Json) + if ($commits.Count -eq 0) { + throw "No commits found on $Org/$Repo ($Branch)" + } + $c = $commits[0] + return @{ + sha = $c.sha + message = $c.commit.message + } +} + +function Get-AzDoCommits { + param( + [string]$Project, + [string]$Repo, + [string]$Branch, + [int]$Top + ) + + $url = "$AzDoOrg/$Project/_apis/git/repositories/$Repo/commits" + + "?searchCriteria.itemVersion.version=$Branch" + + "&searchCriteria.itemVersion.versionType=branch" + + "&`$top=$Top" + + "&api-version=7.1" + + $rawJson = az rest --method get --url $url --resource $AzDoResourceId --only-show-errors 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "AzDo API error for $Project/$Repo ($Branch): $rawJson" + } + $result = $rawJson | ConvertFrom-Json + return @($result.value) +} + +function Test-MirrorStatus { + param( + [hashtable]$Mapping, + [string]$GitHubSha + ) + + $commits = Get-AzDoCommits ` + -Project $Mapping.AzDoProject ` + -Repo $Mapping.AzDoRepo ` + -Branch $Mapping.AzDoBranch ` + -Top $SearchDepth + + if ($commits.Count -eq 0) { + return @{ Mirrored = $false; Details = "AzDo branch has no commits or does not exist" } + } + + # Check for direct push: any AzDo commit has the same SHA as the GitHub commit + foreach ($c in $commits) { + if ($c.commitId -eq $GitHubSha) { + return @{ Mirrored = $true; Details = "Direct push (same SHA on AzDo branch)" } + } + } + + # Check for merge commit: any AzDo commit message references the GitHub SHA + $escapedSha = [regex]::Escape($GitHubSha) + foreach ($c in $commits) { + if ($c.comment -match $escapedSha) { + return @{ Mirrored = $true; Details = "Merge commit references SHA" } + } + } + + # Not mirrored — report the latest AzDo commit for context + $latest = $commits[0] + $latestSha = $latest.commitId.Substring(0, 7) + $latestDate = if ($latest.author.date) { ([datetime]$latest.author.date).ToString('yyyy-MM-dd HH:mm') } else { 'unknown' } + return @{ + Mirrored = $false + Details = "AzDo tip: ``$latestSha`` ($latestDate)" + } +} + +# --- Main --- + +Test-Prerequisites + +$results = @() + +foreach ($m in $Mappings) { + $repo = "$($m.GHOrg)/$($m.GHRepo)" + $branch = $m.GHBranch + + try { + $ghCommit = Get-GitHubLatestCommit -Org $m.GHOrg -Repo $m.GHRepo -Branch $m.GHBranch + $shortSha = $ghCommit.sha.Substring(0, 7) + $firstLine = ($ghCommit.message -split '\r?\n')[0] + $commitDisplay = "``$shortSha`` $(Format-TableCell $firstLine 60)" + + $mirror = Test-MirrorStatus -Mapping $m -GitHubSha $ghCommit.sha + $status = if ($mirror.Mirrored) { '✅' } else { '❌' } + $details = Format-TableCell $mirror.Details + + $results += @{ Repo = $repo; Branch = $branch; Commit = $commitDisplay; Status = $status; Details = $details } + } + catch { + $errMsg = Format-TableCell "$_" + $results += @{ Repo = $repo; Branch = $branch; Commit = 'ERROR'; Status = '⚠️'; Details = $errMsg } + } +} + +# Emit markdown table +Write-Output "## Branch Mirror Status" +Write-Output "" +Write-Output "| Repo | Branch | Latest GH Commit | Mirrored? | Details |" +Write-Output "|---|---|---|---|---|" + +foreach ($r in $results) { + Write-Output "| $($r.Repo) | $($r.Branch) | $($r.Commit) | $($r.Status) | $($r.Details) |" +}