Automate develop to main pull requests

Creating pull requests is part of day-to-day Git work: you propose changes, review them, and merge. Long-lived integration branches make that tedious if someone has to open the release PR by hand every time.

Automatic pull requests keep a develop → main (or master) PR open and refreshed when develop moves. Below are two patterns I use: GitHub Actions with the GitHub CLI, and Azure DevOps with a PowerShell script against the REST API.

GitHub

GitHub Actions can run whenever code is pushed to develop. Prefer create or update so a second push does not fail when the PR already exists — the same idea this site uses in .github/workflows/auto-pr.yml :

name: Auto PR
on:
  push:
    branches:
    - develop
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write

jobs:
  create-pr:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: develop
      - name: Create or update pull request
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TITLE='Merge develop into main'
          BODY='Created by GitHub Actions.'
          if gh pr create -B main -H develop --title "$TITLE" --body "$BODY"; then
            exit 0
          fi
          PR=$(gh pr list --head develop --base main --state open --json number -q '.[0].number')
          if [ -n "$PR" ] && [ "$PR" != "null" ]; then
            gh pr edit "$PR" --title "$TITLE" --body "$BODY"
          else
            echo "No open PR found to update."
            exit 1
          fi

This runs on push to develop, checks out that branch, then creates a PR into main or edits the existing open develop → main PR. Pin actions/checkout to a major tag (or a full commit SHA in production workflows) and grant pull-requests: write.

The workflow uses the GitHub CLI . gh pr create opens the PR; gh pr edit refreshes title and body when create fails because one already exists. You can also fold create-or-edit into a one-liner if you prefer:

run: gh pr create -B main -H develop --title 'Merge develop into main' --body 'Created by GitHub action' || gh pr edit -B main --title "Merge develop into main" --body 'Created by GitHub action'

Azure DevOps

Azure DevOps has a similar feature to GitHub Actions called Azure Pipelines. You can create pipelines that run on specific triggers, such as when code is pushed to a specific branch. For Azure DevOps I have a PowerShell script that does most of the work; let’s have a look at it:

param (
    [Parameter(Mandatory=$true)][string]$repoName,
    [Parameter(Mandatory=$true)][string]$sourceBranch,
    [Parameter(Mandatory=$true)][string]$targetBranch,
    [Parameter(Mandatory=$true)][string]$prTitle,
    [Parameter(Mandatory=$true)][string]$prDescription
)

# Construct base URLs
$collectionUri = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI.TrimEnd('/')
$projectName = $env:SYSTEM_TEAMPROJECT
$apisUrl = "$collectionUri/$projectName/_apis"
$apiVersionQs = "?api-version=7.0"

# Create common headers
$headers = @{
    "Authorization" = "Bearer $env:SYSTEM_ACCESSTOKEN"
    "Content-Type"  = "application/json"
}

# Step 1: Fetch the repository GUID using its name
$repositoryUrl = "$apisUrl/git/repositories/$repoName$apiVersionQs"
$repositoryResponse = Invoke-RestMethod -Uri $repositoryUrl -Method GET -Headers $headers
$repoGuid = $repositoryResponse.id  # Extract the repository GUID

if (-not $repoGuid) {
    throw "Failed to retrieve repository GUID for repository '$repoName'"
}

# Now construct the project URL using the repository GUID
$projectUrl = "$apisUrl/git/repositories/$repoGuid"

# Construct full ref names
$sourceBranchRef = "refs/heads/$sourceBranch"
$targetBranchRef = "refs/heads/$targetBranch"

# Step 2: Prepare the body for the commit comparison using GetCommitsBatch API
$body = @{
    "itemVersion" = @{
        "version" = "$targetBranch"
        "versionType" = "branch"
    }
    "compareVersion" = @{
        "version" = "$sourceBranch"
        "versionType" = "branch"
    }
} | ConvertTo-Json

# Step 3: Call the Azure DevOps API to compare the branches using GetCommitsBatch
$diffUrl = "$projectUrl/commitsBatch$apiVersionQs"
$commitResponse = Invoke-RestMethod -Uri $diffUrl -Method POST -Headers $headers -Body $body

# Step 4: Check if there are any changes between the branches
if ($commitResponse.count -gt 0) {
    Write-Output "There are changes between $sourceBranch and $targetBranch."
} else {
    Write-Output "No changes between $sourceBranch and $targetBranch."
    return
}

# Check for existing active pull requests between source and target branches
$encodedSourceBranchRef = [System.Web.HttpUtility]::UrlEncode($sourceBranchRef)
$encodedTargetBranchRef = [System.Web.HttpUtility]::UrlEncode($targetBranchRef)
$checkPrUrl = "$projectUrl/pullrequests$apiVersionQs&searchCriteria.sourceRefName=$encodedSourceBranchRef&searchCriteria.targetRefName=$encodedTargetBranchRef&searchCriteria.status=active"
Write-Output "Checking for existing active pull requests between $sourceBranch and $targetBranch"
$existingPrs = Invoke-RestMethod -Method GET -Headers $headers -Uri $checkPrUrl
if ($existingPrs.count -gt 0) {
    # An active pull request already exists
    $existingPr = $existingPrs.value[0] # Assuming we take the first one if multiple exist
    $pullRequestId = $existingPr.pullRequestId
    Write-Output "An active pull request already exists between $sourceBranch and $targetBranch."
    Write-Output "Pull Request ID: $pullRequestId"
    Write-Output "##vso[task.setvariable variable=prId]$pullRequestId"    
    return 
}

# Create a Pull Request
$pullRequestUrl = "$projectUrl/pullrequests$apiVersionQs"
$pullRequest = @{
    "sourceRefName" = "$sourceBranchRef"
    "targetRefName" = "$targetBranchRef"
    "title"         = "$prTitle"
    "description"   = "$prDescription"
}

$pullRequestJson = ($pullRequest | ConvertTo-Json -Depth 5)

Write-Output "Sending a REST call to create a new pull request from $sourceBranch to $targetBranch"

# REST call to create a Pull Request
$pullRequestResult = Invoke-RestMethod -Method POST -Headers $headers -Body $pullRequestJson -Uri $pullRequestUrl

# Ensure the pull request ID exists
if (-not $pullRequestResult.pullRequestId) {
    throw "Pull request creation failed. No pull request ID returned."
}

$pullRequestId = $pullRequestResult.pullRequestId
Write-Output "Pull request created. Pull Request Id: $pullRequestId"
Write-Output "##vso[task.setvariable variable=prId]$pullRequestId"

This uses the Azure DevOps REST API to create a PR. The script takes the repository name, source branch, target branch, PR title, and PR description. It compares the branches, creates a PR when there are commits to merge, and reuses an existing active PR when one is already open. Run it as a pipeline step.

My pipeline has a step that looks a bit like:

  - task: PowerShell@2
    displayName: Create PR
    condition: eq(variables['Build.SourceBranch'], 'refs/heads/develop')
    inputs:
      targetType: 'inline'
      script: 'build/CreatePR.ps1 -repoName ProjectX -sourceBranch develop -targetBranch main -prTitle "Release $(Build.Number.Major).$(Build.Number.Minor).x" -prDescription "Dev to Prod"'
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)

I only run this step on the develop branch, and you can see I supply all the parameters that are defined in my PowerShell script. I include the System.AccessToken so the step can call the REST APIs for the project.

For combining repositories before you automate promotion, see merge two projects into one . For a broader GitHub Actions walkthrough, start with using GitHub Actions .

If you have enjoyed this article and want to get a monthly email with all my latest articles, please sign up for my newsletter . If you have any questions or comments, please feel free to reach out or leave a comment below.

Read next

Support this site

Comments