Public/Approve-PSUADOPullRequest.ps1
|
function Approve-PSUADOPullRequest { <# .SYNOPSIS Approves a pull request in Azure DevOps using REST API. .DESCRIPTION This function approves a pull request in Azure DevOps by its ID. It can approve with different vote values and optional comments. You can specify the repository details or use auto-detection from git remote. Requires: Azure DevOps Personal Access Token with appropriate permissions .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 approve. .PARAMETER Vote (Optional) The approval vote value: - 10: Approved - 5: Approved with suggestions - 0: No vote - -5: Waiting for author - -10: Rejected Default value is 10 (Approved). .PARAMETER Comment (Optional) Comment to add with the approval. .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 Approve-PSUADOPullRequest -Organization "omg" -Project "psutilities" -Repository "AzureDevOps" -PullRequestId 123 Approves pull request with ID 123 with default vote (10 - Approved). .EXAMPLE Approve-PSUADOPullRequest -Organization "omg" -Project "psutilities" -Repository "Ai" -PullRequestId 456 -Vote 5 -Comment "Looks good with minor suggestions" Approves pull request with ID 456 with suggestions and a comment. .EXAMPLE Approve-PSUADOPullRequest -Organization "omg" -Project "psutilities" -Repository "Core" -PullRequestId 789 -Vote -5 -Comment "Please address the unit test failures" Sets pull request to "Waiting for author" status with a comment. .OUTPUTS [PSCustomObject] .NOTES Author: Lakshmanachari Panuganti Date: 19th August 2025 .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-request-reviewers/create-pull-request-reviewer #> [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)] [ValidateNotNullOrEmpty()] [int]$PullRequestId, [Parameter()] [ValidateSet(10, 5, 0, -5, -10)] [int]$Vote = 10, [Parameter()] [string]$Comment, [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 { # 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 # Get current user information for the reviewer. # NOTE: Profile APIs are hosted under vssps.dev.azure.com (or app.vssps.visualstudio.com) not dev.azure.com. # The original implementation used dev.azure.com which can return a branded HTML 404 page (as observed by user). $userProfile = $null $profileError = $null $profileEndpointsTried = @() $profileBaseUris = @( "https://vssps.dev.azure.com/$Organization/_apis/profile/profiles/me?api-version=7.0", "https://dev.azure.com/$Organization/_apis/profile/profiles/me?api-version=7.0" # fallback to previous (legacy / misconfigured) just in case ) foreach($endpoint in $profileBaseUris) { $profileEndpointsTried += $endpoint try { Write-Verbose "Attempting profile lookup: $endpoint" $userProfile = Invoke-RestMethod -Method Get -Uri $endpoint -Headers $headers -ErrorAction Stop if($userProfile){ break } } catch { $profileError = $_ Write-Verbose "Profile lookup failed at $endpoint : $($profileError.Exception.Message)" } } if(-not $userProfile) { $msg = "Unable to resolve current user profile from endpoints: `n - " + ($profileEndpointsTried -join "`n - ") + "`nLast error: $($profileError.Exception.Message)" + "`nVerify PAT has 'vso.profile' scope or try passing reviewer user id explicitly (future enhancement)." throw $msg } # Prepare the reviewer body $body = @{ vote = $Vote } if ($Comment) { $body.comment = $Comment } $bodyJson = $body | ConvertTo-Json -Depth 10 $escapedProject = if ($Project -match '%[0-9A-Fa-f]{2}') { $Project } else { [uri]::EscapeDataString($Project) } $reviewerId = $userProfile.id $uri = "https://dev.azure.com/$Organization/$escapedProject/_apis/git/repositories/$repositoryId/pullrequests/$PullRequestId/reviewers/$reviewerId" + "?api-version=7.0" # Determine vote text for display $voteText = switch ($Vote) { 10 { "Approved" } 5 { "Approved with suggestions" } 0 { "No vote" } -5 { "Waiting for author" } -10 { "Rejected" } default { "Vote: $Vote" } } Write-Verbose "Setting approval for pull request ID: $PullRequestId in project: $Project" Write-Verbose "Repository: $Repository ($repositoryId)" Write-Verbose "Vote: $Vote ($voteText)" Write-Verbose "API URI: $uri" $response = Invoke-RestMethod -Method Put -Uri $uri -Headers $headers -Body $bodyJson -ContentType "application/json" -ErrorAction Stop $WebUrl = "https://dev.azure.com/$Organization/$escapedProject/_git/$Repository/pullrequest/$PullRequestId" Write-Host "Pull Request ID $PullRequestId review submitted: $voteText" -ForegroundColor Green Write-Host "PR URL: $WebUrl" -ForegroundColor Cyan if ($Comment) { Write-Host "Comment: $Comment" -ForegroundColor Yellow } # Return structured result [PSCustomObject]@{ Id = $PullRequestId Vote = $response.vote VoteText = $voteText Comment = $Comment ReviewerId = $response.id ReviewerName = $response.displayName ReviewerEmail = $response.uniqueName IsRequired = $response.isRequired Organization = $Organization Project = $Project Repository = $Repository WebUrl = $WebUrl PSTypeName = 'PSU.ADO.PullRequestApproval' } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } |