Public/Complete-PSUADOPullRequest.ps1
|
function Complete-PSUADOPullRequest { <# .SYNOPSIS Completes (merges) a pull request in Azure DevOps using REST API. .DESCRIPTION This function completes a pull request in Azure DevOps by its ID. It supports different merge strategies including merge, squash, and rebase. You can specify the repository details or use auto-detection from git remote. .PARAMETER Organization (Optional) The Azure DevOps organization name under which the project resides. Default value is $env:ORGANIZATION. Set using: Set-PSUUserEnvironmentVariable -Name "ORGANIZATION" -Value "your_org_name" .PARAMETER Project (Mandatory) The Azure DevOps project name containing the repository. .PARAMETER Repository (Mandatory) The repository name containing the pull request. .PARAMETER PullRequestId (Mandatory) The ID of the pull request to complete. .PARAMETER MergeStrategy (Optional) The merge strategy to use: 'merge', 'squash', or 'rebase'. Default value is 'merge'. .PARAMETER DeleteSourceBranch (Optional) Switch parameter to delete the source branch after completion. .PARAMETER CompletionOptions (Optional) Additional completion options as a hashtable. .PARAMETER PAT (Optional) Personal Access Token for Azure DevOps authentication. Default value is $env:PAT. Set using: Set-PSUUserEnvironmentVariable -Name "PAT" -Value "your_pat_token" .EXAMPLE Complete-PSUADOPullRequest -Organization "omg" -Project "psutilities" -Repository "AzureDevOps" -PullRequestId 123 Completes pull request with ID 123 using default merge strategy. .EXAMPLE Complete-PSUADOPullRequest -Organization "omg" -Project "psutilities" -Repository "Ai" -PullRequestId 456 -MergeStrategy "squash" Completes pull request with ID 456 using squash merge strategy. .EXAMPLE Complete-PSUADOPullRequest -Organization "omg" -Project "psutilities" -Repository "Core" -PullRequestId 789 -DeleteSourceBranch Completes pull request with ID 789 and deletes the source branch. .OUTPUTS [PSCustomObject] .NOTES Author: Lakshmanachari Panuganti Date: 19th August 2025 Requires: Azure DevOps Personal Access Token with appropriate permissions .LINK https://github.com/lakshmanachari-panuganti/OMG.PSUtilities/tree/main/OMG.PSUtilities.AzureDevOps https://www.linkedin.com/in/lakshmanachari-panuganti/ https://www.powershellgallery.com/packages/OMG.PSUtilities.AzureDevOps https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/update #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidUsingWriteHost', '', Justification = 'This is intended for this function to display formatted output to the user on the console' )] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Project, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Repository, [Parameter(Mandatory)] [ValidateRange(1, [int]::MaxValue)] [int]$PullRequestId, [Parameter()] [ValidateSet('merge', 'squash', 'rebase', 'rebaseMerge')] [string]$MergeStrategy = 'merge', [Parameter()] [switch]$DeleteSourceBranch, [Parameter()] [hashtable]$CompletionOptions, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Organization = $env:ORGANIZATION, [Parameter()] [ValidateNotNullOrEmpty()] [string]$PAT = $env:PAT ) begin { # Display parameters Write-Verbose "[$($MyInvocation.MyCommand.Name)] Parameters:" foreach ($param in $PSBoundParameters.GetEnumerator()) { if ($param.Key -eq 'PAT') { $maskedPAT = if ($param.Value -and $param.Value.Length -ge 3) { $param.Value.Substring(0, 3) + "********" } else { "***" } Write-Verbose " $($param.Key): $maskedPAT" } else { Write-Verbose " $($param.Key): $($param.Value)" } } # Validate Organization (required because ValidateNotNullOrEmpty doesn't check default values from environment variables) if (-not $Organization) { throw "The default value for the 'ORGANIZATION' environment variable is not set.`nSet it using: Set-PSUUserEnvironmentVariable -Name 'ORGANIZATION' -Value '<org>' or provide via -Organization parameter." } # Validate PAT (required because ValidateNotNullOrEmpty doesn't check default values from environment variables) if (-not $PAT) { throw "The default value for the 'PAT' environment variable is not set.`nSet it using: Set-PSUUserEnvironmentVariable -Name 'PAT' -Value '<pat>' or provide via -PAT parameter." } $headers = Get-PSUAdoAuthHeader -PAT $PAT } process { try { # Escape project name for URI $escapedProject = if ($Project -match '%[0-9A-Fa-f]{2}') { $Project } else { [uri]::EscapeDataString($Project) } # Get repository ID $repos = Get-PSUADORepositories -Project $Project -Organization $Organization -PAT $PAT $matchedRepo = $repos | Where-Object { $_.Name -eq $Repository } if (-not $matchedRepo) { throw "Repository '$Repository' not found in project '$Project'." } $repositoryId = $matchedRepo.Id # First, get the current PR details $getPrUri = "https://dev.azure.com/$Organization/$escapedProject/_apis/git/repositories/$repositoryId/pullrequests/$PullRequestId" + "?api-version=7.0" Write-Verbose "Getting pull request details from: $getPrUri" $currentPr = Invoke-RestMethod -Method Get -Uri $getPrUri -Headers $headers -ErrorAction Stop # Prepare completion options (use a fresh ordered hashtable to avoid any collection mutation issues later) $completionOptions = [ordered]@{} if ($MergeStrategy -eq 'merge') { $completionOptions.mergeStrategy = 'noFastForward' } elseif ($MergeStrategy -eq 'squash') { $completionOptions.mergeStrategy = 'squash' } elseif ($MergeStrategy -eq 'rebase') { $completionOptions.mergeStrategy = 'rebase' } elseif ($MergeStrategy -eq 'rebaseMerge') { $completionOptions.mergeStrategy = 'rebaseMerge' } if ($DeleteSourceBranch) { $completionOptions.deleteSourceBranch = $true } # Add any additional completion options (snapshot keys first to avoid 'collection modified' issues if caller mutates outside) if ($CompletionOptions) { $additional = @{} foreach ($entry in @($CompletionOptions.GetEnumerator())) { # snapshot enumeration safely $additional[$entry.Key] = $entry.Value } foreach ($k in $additional.Keys) { $completionOptions[$k] = $additional[$k] } } # Determine source commit id; fall back defensively if property missing $sourceCommitId = $null if ($currentPr.PSObject.Properties.Name -contains 'lastMergeSourceCommit' -and $currentPr.lastMergeSourceCommit.commitId) { $sourceCommitId = $currentPr.lastMergeSourceCommit.commitId } elseif ($currentPr.PSObject.Properties.Name -contains 'lastMergeCommit' -and $currentPr.lastMergeCommit.commitId) { $sourceCommitId = $currentPr.lastMergeCommit.commitId } else { Write-Verbose "Source commit id not found on PR object; proceeding without lastMergeSourceCommit optimistic concurrency check." } $bodyObject = [ordered]@{ status = 'completed' completionOptions = $completionOptions } if ($sourceCommitId) { $bodyObject.lastMergeSourceCommit = @{ commitId = $sourceCommitId } } $body = $bodyObject | ConvertTo-Json -Depth 10 Write-Verbose "Completion payload: $body" $uri = "https://dev.azure.com/$Organization/$escapedProject/_apis/git/repositories/$repositoryId/pullrequests/$PullRequestId" + "?api-version=7.0" Write-Verbose "Completing pull request ID: $PullRequestId in project: $Project" Write-Verbose "Repository: $Repository ($repositoryId)" Write-Verbose "Merge strategy: $MergeStrategy" Write-Verbose "API URI: $uri" try { $response = Invoke-RestMethod -Method Patch -Uri $uri -Headers $headers -Body $body -ContentType "application/json" -ErrorAction Stop } catch { Write-Verbose "Raw failure body (if any) following completion attempt." if ($_.Exception.Response) { try { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $raw = $reader.ReadToEnd() Write-Verbose $raw } catch {} } throw } $WebUrl = "https://dev.azure.com/$Organization/$escapedProject/_git/$Repository/pullrequest/$PullRequestId" Write-Host "Pull Request ID $PullRequestId completed successfully!" -ForegroundColor Green Write-Host "PR URL: $WebUrl" -ForegroundColor Cyan Write-Host "Merge strategy: $MergeStrategy" -ForegroundColor Yellow if ($DeleteSourceBranch) { Write-Host "Source branch will be deleted." -ForegroundColor Green } # Return structured result [PSCustomObject]@{ Id = $response.pullRequestId Status = $response.status Title = $response.title MergeStrategy = $MergeStrategy SourceBranch = $response.sourceRefName TargetBranch = $response.targetRefName CompletedBy = $response.closedBy.displayName CompletionDate = $response.closedDate MergeId = $response.mergeId DeletedSourceBranch = $DeleteSourceBranch.IsPresent Organization = $Organization Project = $Project Repository = $Repository WebUrl = $WebUrl PSTypeName = 'PSU.ADO.PullRequestCompletion' } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } |