Search code examples
azure-devopscontinuous-integrationpull-requestbuild-triggers

Don't trigger builds for branches that already have a pull request in Azure DevOps


We use Azure DevOps for continuous integration. The pipeline is configured to run a build whenever a change is pushed to a feature branch. This is desired for quick feedback.

Additionally, we have the policy for the master branch that a successful validation build is required before a feature branch can be merged. Azure DevOps now automatically triggers the corresponding validation build when a pull request (PR) is created for a feature branch.

All of this is fine, but there is an adversity: if a PR is already created and the feature branch is updated, two builds are triggered (one for the feature branch alone and one for the outcome of the merge, i.e., the validation build).

I understand that some people might want both builds, but in our case (an probably in every normal case) it would be better if only the validation build was triggered.

Question: Is there a way to tell Azure DevOps that it should ignore branch triggers for any branch that already has a PR? Workarounds with an equivalent outcome are also welcome, of course.

The question has already been posted as an issue here, but I could not find a satisfying answer in the replies (e.g., branch filters and a naming strategy do not solve the problem).


Solution

  • I have solved the issue like suggested by Shamrai. I'm adding this as another answer to share my code.

    Add the following PowerShell code to a step at the beginning of the pipeline. It uses the Azure DevOps REST API to check for an existing PR of the current branch. If there is no such PR or if the current build was manually queued, or if the PR is not ready to be merged (e.g. conflicts), the build continues as normal. Otherwise, it is cancelled.

    $BaseApiUri_Builds   = "https://my.tfs.server.com/MyCollection/MyProject/_apis/build/builds"
    $BaseApiUri_GitRepos = "https://my.tfs.server.com/MyCollection/MyProject/_apis/git/repositories"
    $AuthenicationHeader = @{ Authorization = "Bearer ${env:System_AccessToken}" }
    
    # Cancels the current build
    function Cancel-This-Build()
    {
        Cancel-Build -BuildId ${env:Build_BuildId}
    }
    
    # Cancels the given build
    function Cancel-Build([String] $BuildId)
    {
        Write-Host "Cancelling build ${BuildId}..."
        
        $BuildApiUri = "${BaseApiUri_Builds}/${BuildId}?api-version=5.1"
        $CancelBody = @{ status = "cancelling" } | ConvertTo-Json
        Invoke-RestMethod -Uri $BuildApiUri -Method PATCH -ContentType application/json -Body $CancelBody -Header $AuthenicationHeader
    }
    
    # Detects if a validation build is queued for the given branch. This is the case if an active PR exists that does not have merge conflicts.
    function Check-For-Validation-Build([String] $BranchName)
    {
        Write-Host "Checking for validation builds of branch '${BranchName}' in repository ${env:Build_Repository_ID}..."
        
        $GetPRsApiUri = "${BaseApiUri_GitRepos}/${env:Build_Repository_ID}/pullrequests?api-version=5.1&searchCriteria.sourceRefName=${BranchName}"
        $PRs = Invoke-RestMethod -Uri $GetPRsApiUri -Method GET -Header $AuthenicationHeader
        
        ForEach($PR in $PRs.Value)
        {
            Write-Host "Found PR $($PR.pullRequestId) '$($PR.title)': isDraft=$($PR.isDraft), status=$($PR.status), mergeStatus=$($PR.mergeStatus)"
            
            if (!$PR.isDraft -and ($PR.mergeStatus -eq "succeeded") -and ($PR.status -eq "active"))
            {
                Write-Host "Validation build is queued for branch '${BranchName}'."
                return $true
            }
        }
        
        Write-Host "No validation build is queued for branch '${BranchName}'."
        return $false
    }
    
    # Check if a valid PR exists. If so, cancel this build, because a validation build is also queued. 
    $HasValidationBuild = Check-For-Validation-Build -BranchName ${env:Build_SourceBranch}
    if (($HasValidationBuild -eq $true) -and (${env:Build_Reason} -ne "Manual") -and !${env:Build_SourceBranch}.EndsWith('/merge'))
    {
        Cancel-This-Build
    }
    

    Note that the System.AccessToken environment variable must be made visible for the script to work.