Introduction
In my experience, there are two main reasons stale branches exist in Azure DevOps (or any source code repo):
- Branches are not deleted after completing pull request. Although Azure DevOps gives you option to delete branch after merging given the right permissions are applied. But this option is not used most of the time
- Developers create temp branches for proof of concepts, resolve merging conflicts or any other reason and then forget to delete them
Prerequisite
a. Install the Azure Cli
b. Install the Azure Cli DevOps extension through PowerShell by running following command
$ az extension add --name azure-devops
Write deletion scripts
Start PowerShell and login to Azure
$ az login
a. Script to delete branches that have completed the pull request (delete-pull-request-completed-branches.ps1)
$project = "{projectName}"
$repository = "{repoName}"
if (-not (Test-Path env:IS_DRY_RUN)) { $env:IS_DRY_RUN = $true }
Write-Host ("is dry run: {0}" -f $env:IS_DRY_RUN)
$prsCompleted = az repos pr list `
--project $project `
--repository $repository `
--target-branch develop `
--status completed `
--query "[].sourceRefName" |
ConvertFrom-Json;
if ($prsCompleted.count -eq 0) {
Write-Host "No merged pull request"
return;
}
$refs = az repos ref list --project $project --repository $repository --filter heads | ConvertFrom-Json
$refs |
Where-Object { $prsCompleted.Contains( $_.name ) } |
ForEach-Object {
Write-Host ("deleting merged branch: {0} - {1}" -f $_.name, $_.objectId)
if (![System.Convert]::ToBoolean($env:IS_DRY_RUN)) {
$result = az repos ref delete `
--name $_.name `
--object-id $_.objectId `
--project $project `
--repository $repository |
ConvertFrom-Json
Write-Host ("success message: {0}" –f $result.updateStatus)
}
}
Notes:
- Replace {projectName} and {repoName} with your own project and repository name
- Use IS_DRY_RUN environment variable to list the branches without deleting them. It can be changed manually when running on local machine or can be setup as Azure DevOps pipeline build variable
b. Script to delete old, staled branches (delete-stale-branches.ps1)
$project = "projectName"
$repository = "repoName"
$excludeBranches = @("develop", "master")
$daysDeleteBefore = -30
$dateTimeNow = [DateTime]::Now
$dateTimeBeforeToDelete = $dateTimeNow.AddDays( $daysDeleteBefore)
if (-not (Test-Path env:IS_DRY_RUN)) { $env:IS_DRY_RUN = $true }
Write-Host ("is dry run: {0}" -f $env:IS_DRY_RUN)
Write-Host ("datetime now: {0}" -f $dateTimeNow)
Write-Host ("delete branches before {0}" -f (get-date $dateTimeBeforeToDelete))
$refs = az repos ref list --project $project --repository $repository --filter heads | ConvertFrom-Json
$toDeleteBranches = @()
foreach ($ref in $refs) {
if ($ref.name -replace "refs/heads/" -in $excludeBranches) {
continue;
}
$objectId = $ref.objectId
# fetch individual commit details
$commit = az devops invoke `
--area git `
--resource commits `
--route-parameters `
project=$project `
repositoryId=$repository `
commitId=$objectId |
ConvertFrom-Json
$toDelete = [PSCustomObject]@{
objectId = $objectId
name = $ref.name
creator = $ref.creator.uniqueName
lastAuthor = $commit.committer.email
lastModified = $commit.push.date
}
$toDeleteBranches += , $toDelete
}
$toDeleteBranches = $toDeleteBranches | Where-Object { (get-date $_.lastModified) -lt (get-date $dateTimeBeforeToDelete) }
if ($toDeleteBranches.count -eq 0) {
Write-Host "No stale branches to delete"
return;
}
$toDeleteBranches |
ForEach-Object {
Write-Host ("deleting staled branch: name={0} - id={1} - lastModified={2}" -f $_.name, $_.objectId, $_.lastModified)
if (![System.Convert]::ToBoolean($env:IS_DRY_RUN)) {
$result = az repos ref delete `
--name $_.name `
--object-id $_.objectId `
--project $project `
--repository $repository |
ConvertFrom-Json
Write-Host ("success message: {0}" -f $result.updateStatus)
}
}
Notes:
- Use $excludeBranches for excluding branches you do not want to delete
- Use $daysDeleteBefore for indicating the number of days before to delete branches
Define the Azure DevOps pipeline (azure-pipelines.yml)
name: Delete Branches
schedules:
# run at midnight every day
- cron: "0 0 * * *"
displayName: Delete branches
branches:
include:
- develop
pool:
vmImage: ubuntu-latest
steps:
- script: |
az extension add -n azure-devops
displayName: Install Azure DevOps CLI
- checkout: self
clean: true
- script: |
echo $(ACCESS_TOKEN) | az devops login
displayName: Login
env:
ADO_PAT_TOKEN: $(ACCESS_TOKEN)
- pwsh: .\delete-pull-request-completed-branches.ps1
displayName: 'Delete merged branches'
- pwsh: .\delete-stale-branches.ps1
displayName: 'Delete stale branches'
Notes:
- Schedule is defined to run the job at midnight
- ubuntu-latest is used as build machine
- First step install the Azure DevOps extension
- Second step login to Azure DevOps using PAT token. ACCESS_TOKEN should be defined as as Azure DevOps pipeline build variable
- Third step run the script to delete merged pull requests
- Fourth step run the script to delete stale branches
Resources: