Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions tools/AzDev/AzDev/AzDev.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 = @()
Expand Down
198 changes: 198 additions & 0 deletions tools/AzDev/AzDev/GitHub.psm1
Original file line number Diff line number Diff line change
@@ -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')
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

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

[nitpick] Using a culture-dependent format ('M/d/yyyy h:mm:ss tt') produces ambiguous dates and hampers machine parsing. Prefer an invariant/ISO 8601 format such as ToString('yyyy-MM-ddTHH:mm:ssZ') (after ensuring UTC) or return the DateTime object directly for consumers to format.

Suggested change
'CreatedAt' = [DateTime]::Parse($_.createdAt).ToString('M/d/yyyy h:mm:ss tt')
'CreatedAt' = ([DateTime]::Parse($_.createdAt).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

@isra-fel I believe this could be a valid point.

'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]@{
Comment on lines +180 to +188
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

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

The function throws (line 183) before reaching the return block (line 186) when any PR fails, contradicting the documented behavior that successfully merged PRs are still returned. Move the return construction before the throw or replace the throw with a non-terminating Write-Error so merged PRs are output. Example fix: build the result object, Write-Error if failures exist, then return the result without throwing.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is valid as there is no wrapping catch.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated: merged PRs are returned before the errors are thrown

'No.' = $_.number
'Title' = $_.title
'CreatedBy' = $_.author.login
'CreatedAt' = [DateTime]::Parse($_.createdAt).ToString('M/d/yyyy h:mm:ss tt')
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

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

[nitpick] Using a culture-dependent format ('M/d/yyyy h:mm:ss tt') produces ambiguous dates and hampers machine parsing. Prefer an invariant/ISO 8601 format such as ToString('yyyy-MM-ddTHH:mm:ssZ') (after ensuring UTC) or return the DateTime object directly for consumers to format.

Suggested change
'CreatedAt' = [DateTime]::Parse($_.createdAt).ToString('M/d/yyyy h:mm:ss tt')
'CreatedAt' = ([DateTime]::Parse($_.createdAt).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
'CreatedAt' = [DateTime]::Parse($_.createdAt).ToString('M/d/yyyy h:mm:ss tt')
'CreatedAt' = [DateTime]::Parse($_.createdAt).ToString('M/d/yyyy h:mm:ss tt')

'Url' = $_.url
}
}
}

Export-ModuleMember -Function Merge-DevPullRequest -Alias Merge-DevPR
3 changes: 3 additions & 0 deletions tools/AzDev/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
33 changes: 33 additions & 0 deletions tools/AzDev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

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

Documentation states merged PRs are still returned when an error occurs, but current implementation throws before returning (see lines 180-188 in GitHub.psm1). Update the code to return results on partial failure or adjust this sentence to reflect the actual behavior.

Suggested change
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.
The cmdlet returns the list of merged PRs. If any PR fails to merge, an error is thrown and no results are returned.

Copilot uses AI. Check for mistakes.

```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
Expand Down
Loading