diff --git a/tools/AzDev/AzDev/AzDev.psd1 b/tools/AzDev/AzDev/AzDev.psd1 index 16357d7a65c2..8933670c5cab 100644 --- a/tools/AzDev/AzDev/AzDev.psd1 +++ b/tools/AzDev/AzDev/AzDev.psd1 @@ -67,10 +67,11 @@ FormatsToProcess = @('AzDev.format.ps1xml') # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess NestedModules = @('bin/AzDev.dll', - 'CommonRepo.psm1') + 'CommonRepo.psm1', + 'GitHub.psm1') # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Connect-DevCommonRepo', 'Disconnect-DevCommonRepo' +FunctionsToExport = 'Connect-DevCommonRepo', 'Disconnect-DevCommonRepo', 'Merge-DevPullRequest' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = 'Get-DevContext', 'Set-DevContext', @@ -82,7 +83,7 @@ CmdletsToExport = 'Get-DevContext', 'Set-DevContext', VariablesToExport = '*' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' +AliasesToExport = 'Merge-DevPR' # DSC resources to export from this module # DscResourcesToExport = @() diff --git a/tools/AzDev/AzDev/GitHub.psm1 b/tools/AzDev/AzDev/GitHub.psm1 new file mode 100644 index 000000000000..daacc4de464e --- /dev/null +++ b/tools/AzDev/AzDev/GitHub.psm1 @@ -0,0 +1,198 @@ +function Merge-DevPullRequest { + <# + .SYNOPSIS + Merges pull requests in the azure-powershell repository. + + .DESCRIPTION + Helps merge pull requests in the azure-powershell repo. When using -AllArchivePR, for safety, + it only supports merging PRs with the "[skip ci]" prefix in the title and created by the + "azure-powershell-bot" user, which are the archive PRs for generated modules. + When using -Number, any PR can be merged. + + .PARAMETER Number + The pull request number(s) to merge. Can be a single number or an array of numbers. + + .PARAMETER AllArchivePR + Lists all matching PRs (ordered by CreatedAt ascending) and prompts for confirmation before merging. + + .PARAMETER Approve + Approve the pull request before merging. + + .PARAMETER Force + Skip confirmation prompts. + + .EXAMPLE + Merge-DevPullRequest -Approve -Number 28690 + + .EXAMPLE + Merge-DevPullRequest -Approve -Number 28690, 28691, 28692 + + .EXAMPLE + Merge-DevPullRequest -Approve -AllArchivePR -Force + + .NOTES + Requires GitHub CLI to be installed and authenticated (gh auth login). + #> + [CmdletBinding(DefaultParameterSetName = 'Single')] + [Alias('Merge-DevPR')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Single')] + [int[]]$Number, + + [Parameter(Mandatory = $true, ParameterSetName = 'All')] + [switch]$AllArchivePR, + + [switch]$Approve, + + [switch]$Force + ) + + # Check if GitHub CLI is available + try { + gh --version | Out-Null + } + catch { + throw "GitHub CLI is not installed or not in PATH. Please install GitHub CLI and authenticate with 'gh auth login'." + } + + # Check if authenticated + try { + gh auth status | Out-Null + } + catch { + throw "GitHub CLI is not authenticated. Please run 'gh auth login' first." + } + + $mergedPRs = @() + $failedPRs = @() + + if ($PSCmdlet.ParameterSetName -eq 'Single') { + # Get PR details for each number + $prsToMerge = @() + foreach ($prNumber in $Number) { + try { + $prJson = gh pr view $prNumber --json number,title,author,createdAt,url 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Pull request #$prNumber not found." + } + $pr = $prJson | ConvertFrom-Json + $prsToMerge += $pr + } + catch { + throw "Failed to get pull request #$prNumber: $_" + } + } + } + else { + # Get all archive PRs + try { + $allPRsJson = gh pr list --state open --author azure-powershell-bot --json number,title,author,createdAt,url 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Failed to list pull requests." + } + $allPRs = $allPRsJson | ConvertFrom-Json + } + catch { + throw "Failed to list pull requests: $_" + } + + # Filter for archive PRs (with [skip ci] prefix) + $archivePRs = $allPRs | Where-Object { $_.title.StartsWith('[skip ci]') } | Sort-Object createdAt + + if ($archivePRs.Count -eq 0) { + Write-Host "No archive PRs found matching criteria." + return @() + } + + $prsToMerge = $archivePRs + } + + # Display PRs to be merged + if ($prsToMerge.Count -gt 0) { + if (-not $Force) { + $confirmMessage = "Do you want to" + if ($Approve) { $confirmMessage += " approve and" } + $confirmMessage += " merge the following pull request$(if($prsToMerge.Count -gt 1){'s'})?" + Write-Host $confirmMessage + } + + # Format and display PRs + $prTable = $prsToMerge | ForEach-Object { + [PSCustomObject]@{ + 'No.' = $_.number + 'Title' = $_.title + 'CreatedBy' = $_.author.login + 'CreatedAt' = [DateTime]::Parse($_.createdAt).ToString('M/d/yyyy h:mm:ss tt') + 'Url' = $_.url + } + } + + $prTable | Format-Table -AutoSize | Out-String | Write-Host + + # Confirmation prompt (for both parameter sets unless Force is used) + if (-not $Force) { + do { + $response = Read-Host "Type Y to$(if($Approve){' approve and'}) merge, N to cancel" + } while ($response -notin @('Y', 'y', 'N', 'n')) + + if ($response -in @('N', 'n')) { + Write-Host "Operation cancelled." + return @() + } + } + } + + # Merge PRs + foreach ($pr in $prsToMerge) { + try { + Write-Host "Processing PR #$($pr.number)..." -ForegroundColor Yellow + + # Approve if requested + if ($Approve) { + Write-Host " Approving PR #$($pr.number)..." -ForegroundColor Cyan + gh pr review $pr.number --approve 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Failed to approve PR #$($pr.number)" + } + } + + # Merge PR + Write-Host " Merging PR #$($pr.number)..." -ForegroundColor Cyan + gh pr merge $pr.number --squash 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Failed to merge PR #$($pr.number)" + } + + Write-Host " Successfully merged PR #$($pr.number)" -ForegroundColor Green + $mergedPRs += $pr + } + catch { + Write-Error "Failed to merge PR #$($pr.number): $_" + $failedPRs += $pr + } + } + + # Report results + if ($mergedPRs.Count -gt 0) { + Write-Host "`nSuccessfully merged $($mergedPRs.Count) pull request(s)." -ForegroundColor Green + } + + if ($failedPRs.Count -gt 0) { + $errorMessage = "Failed to merge $($failedPRs.Count) pull request(s): $($failedPRs.number -join ', ')" + Write-Error $errorMessage + throw $errorMessage + } + + # Return merged PRs in the format shown in README + return $mergedPRs | ForEach-Object { + [PSCustomObject]@{ + 'No.' = $_.number + 'Title' = $_.title + 'CreatedBy' = $_.author.login + 'CreatedAt' = [DateTime]::Parse($_.createdAt).ToString('M/d/yyyy h:mm:ss tt') + 'Url' = $_.url + } + } +} + +Export-ModuleMember -Function Merge-DevPullRequest -Alias Merge-DevPR diff --git a/tools/AzDev/CHANGELOG.md b/tools/AzDev/CHANGELOG.md index f124d2a2698c..c01478f5b645 100644 --- a/tools/AzDev/CHANGELOG.md +++ b/tools/AzDev/CHANGELOG.md @@ -10,6 +10,9 @@ - Quick start templates - Versioning and publishing AzDev module +## 2025/10/20 +- Feature: `Merge-DevPullRequest` cmdlet to help merge PRs in azure-powershell repo. + ## 2025/8/26 - Feature: Recognize AutoRest.PowerShell version (v3/v4) for AutoRest-based projects and show as `SubType` in `Get-DevProject` output. diff --git a/tools/AzDev/README.md b/tools/AzDev/README.md index 90835b189f19..2a4cf5c9da45 100644 --- a/tools/AzDev/README.md +++ b/tools/AzDev/README.md @@ -13,6 +13,8 @@ Like many other tools, this module targets `net8.0` so always run it in PowerShe - [Connect azure-powershell and azure-powershell-common](#connect-azure-powershell-and-azure-powershell-common) - [Autorest helper](#autorest-helper) - [Open swagger online](#open-swagger-online) + - [GitHub Helpers](#github-helpers) + - [Merge PRs](#merge-prs) - [Development](#development) - [Design](#design) - [Testing](#testing) @@ -116,6 +118,37 @@ Enter the number corresponding to your selection Opening https://github.com/Azure/azure-rest-api-specs/blob/202321f386ea5b0c103b46902d43b3d3c50e029c/specification/workloads/resource-manager/Microsoft.Workloads/SAPVirtualInstance/readme.md in default browser... ``` +### GitHub Helpers + +Prerequisite: You need to install [GitHub CLI](https://cli.github.com/) and set up GitHub authentication first with `gh auth login`. + +#### Merge PRs + +`Merge-DevPullRequest` (alias: `Merge-DevPR`) helps you merge pull requests in the azure-powershell repo. It supports two parameter sets: merging a specific PR by number, and merging all archive PRs. + +With the `-AllArchivePR` switch, the cmdlet lists all the matching PRs, ordered by CreatedAt ascending, and prompts you to confirm before merging. Use the `-Force` switch to skip the confirmation. For safety, this parameter set only supports merging PRs with the "[skip ci]" prefix in the title and created by the `azure-powershell-bot` user, which are the archive PRs for generated modules. + +The cmdlet returns the list of merged PRs. In case any PR fails to merge, an error is thrown, and the successfully merged PRs are still returned. + +```powershell +PS C:\> Merge-DevPullRequest -Approve -Number 28690 + +Do you want to approve and merge the following pull request? +- 28690 [skip ci] Archive e36b0a91ac13ad8c173760dab2d1c038495d41cc +Type Y to approve and merge, N to cancel: Y + +No. Title CreatedBy CreatedAt Url +--- ----- --------- --------- --- +28690 [skip ci] Archive e36b0a91ac13ad8c173760dab2d1c038495d41cc azure-powershell-bot 6/10/2024 2:15:30 PM + +PS C:\> Merge-DevPullRequest -Approve -AllArchivePR -Force + +No. Title CreatedBy CreatedAt Url +--- ----- --------- --------- --- +28690 [skip ci] Archive e36b0a91ac13ad8c173760dab2d1c038495d41cc azure-powershell-bot 6/10/2024 2:15:30 PM +28689 [skip ci] Archive deed8db801365cc26557b46c7a8a01d134f0b524 azure-powershell-bot 6/29/2024 11:05:12 AM +``` + ## Development ### Design