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.
Related on this blog
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.
Comments