
Maintaining a clean and organized repository is crucial for efficient development workflows. Over time, stale branches accumulate, cluttering the repository and making it difficult to manage. Additionally, branches that have been merged into the main branch may still linger around, adding to the clutter. Automating the cleanup of these branches can save time and ensure that your repository remains tidy. In this blog post, we’ll walk through PowerShell scripts that automate the deletion of both stale and merged branches in an Azure DevOps repository.
Azure DevOps also provides an option to delete the source branch after the PR is merged, but sometimes this option is not selected. As a result, these branches can become stale if there is no schedule to delete them. Here, we will provide a sample of how to add a schedule in an Azure DevOps pipeline to delete stale branches frequently.
Why Automate Branch Cleanup?
Manually managing branches can be tedious and error-prone. Automating the process:
- Ensures consistent and regular cleanup.
- Reduces the risk of human error.
- Saves time and effort.
- Keeps the repository clean and manageable.
Prerequisites:
- Install the Azure CLI.
- Install the Azure CLI DevOps extension through PowerShell by running the following command:
az extension add --name azure-devops
Automating the Cleanup of Merged Branches
Below is a PowerShell script that automates the deletion of branches that have been merged into the master branch. This script fetches completed pull requests targeting the master branch and deletes the corresponding source branches.
# Set the IS_DRY_RUN environment variable to $true (or $false if you want to actually delete branches)
$env:IS_DRY_RUN = $true
$project = "projectName"
$repository = "repositoryName"
$organisation = "https://dev.azure.com/orgName/"
if (-not (Test-Path env:IS_DRY_RUN)) { $env:IS_DRY_RUN = $true }
Write-Host ("is dry run: {0}" -f $env:IS_DRY_RUN)
# Debug: Check the PR list output
Write-Host "Fetching completed pull requests targeting 'master' branch..."
$prsCompleted = az repos pr list `
--project $project `
--repository $repository `
--organization $organisation `
--target-branch master `
--status completed `
--query "[].sourceRefName" |
ConvertFrom-Json
# Debug: Print the raw output
Write-Host "Completed PRs (raw):"
Write-Host $prsCompleted
if ($prsCompleted.count -eq 0) {
Write-Host "No merged pull request"
return
} else {
Write-Host "Found pull request"
}
# Debug: Check the refs output
Write-Host "Fetching refs..."
$refs = az repos ref list --project $project --repository $repository --organization $organisation --filter heads | ConvertFrom-Json
# Debug: Print the refs output
Write-Host "Refs (raw):"
Write-Host $refs
$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)
}
}
How the Script Works
- Setting the
IS_DRY_RUNVariable: The script begins by setting theIS_DRY_RUNvariable. This variable controls whether the script actually deletes branches or simply performs a dry run. You can set theIS_DRY_RUNvariable at the pipeline level to control whether the script performs a dry run or actually deletes the branches. - Fetching Completed Pull Requests: The script fetches a list of completed pull requests that target the master branch. This is done using the Azure DevOps CLI.
- Checking for Merged Pull Requests: If there are no merged pull requests, the script exits. Otherwise, it proceeds to the next step.
- Fetching Branch References: The script fetches all branch references (
refs) from the repository. These are then converted from JSON format. - Deleting Merged Branches: The script filters the branches based on the completed pull requests and deletes the corresponding branches. If the
IS_DRY_RUNvariable is set totrue, the script only prints the branches that would be deleted. If set tofalse, it proceeds to delete the merged branches.
Automating the Cleanup of Stale Branches
In addition to deleting merged branches, you can also automate the cleanup of stale branches that have not been modified for a certain number of days. Below is a PowerShell script for deleting branches that have not been modified in the last 30 days.
# Set the IS_DRY_RUN environment variable to $true (or $false if you want to actually delete branches)
$env:IS_DRY_RUN = $true
$project = "projectName"
$repository = "repositoryName"
$organisation = "https://dev.azure.com/orgName/"
$excludeBranches = @("develop", "master", "release")
$daysDeleteBefore = -30
$dateTimeNow = [DateTime]::Now
$dateTimeBeforeToDelete = $dateTimeNow.AddDays($daysDeleteBefore)
if (-not (Test-Path env:IS_DRY_RUN)) { $env:IS_DRY_RUN = $true }
# Print statements for debugging
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))
# Fetch refs
Write-Host "Fetching refs..."
$refs = az repos ref list --project $project --repository $repository --organization $organisation --filter heads | ConvertFrom-Json
# Print total refs count
Write-Host ("Total refs fetched: {0}" -f $refs.Count)
$branchIndex = 1
foreach ($ref in $refs) {
Write-Host ("{0}. Processing ref: {1}" -f $branchIndex, $ref.name)
if ($ref.name -replace "refs/heads/" -in $excludeBranches) {
Write-Host ("Skipping excluded branch: {0}" -f $ref.name)
continue
}
$objectId = $ref.objectId
# Fetch individual commit details
Write-Host ("Fetching commit details for: {0}" -f $objectId)
$commit = az devops invoke `
--area git `
--resource commits `
--route-parameters `
project=$project `
repositoryId=$repository `
organization=$organisation `
commitId=$objectId `
--organization $organisation |
ConvertFrom-Json
# Print commit details
Write-Host ("Fetched commit details for: {0}, Committer: {1}, Push date: {2}" -f $objectId, $commit.committer.email, $commit.push.date)
# Determine if the branch should be deleted
$lastModifiedDate = [DateTime]::Parse($commit.push.date)
if ($lastModifiedDate -lt $dateTimeBeforeToDelete) {
Write-Host ("{0}. Deleting stale branch: name={1} - id={2} - lastModified={3}" -f $branchIndex, $ref.name, $objectId, $commit.push.date)
if (![System.Convert]::ToBoolean($env:IS_DRY_RUN)) {
$result = az repos ref delete `
--name $ref.name `
--object-id $objectId `
--project $project `
--organization $organisation `
--repository $repository |
ConvertFrom-Json
Write-Host ("success message: {0}" -f $result.updateStatus)
}
} else {
Write-Host ("Branch {0} is not stale and will not be deleted." -f $ref.name)
}
$branchIndex++
}
Write-Host "Script execution completed."
How the Script Works
- Setting the
IS_DRY_RUNVariable: The script begins by setting theIS_DRY_RUNvariable. This variable controls whether the script actually deletes branches or simply performs a dry run. - Fetching Branch References: The script fetches all branch references (
refs) from the repository. These are then converted from JSON format. - Processing Each Branch: For each branch, the script fetches commit details to determine the last modification date.
- Filtering Stale Branches: The script filters branches that have not been modified for a specified number of days (30 days in this case).
- Deleting Stale Branches: The script deletes the filtered stale branches. If the
IS_DRY_RUNvariable is set totrue, the script only prints the branches that would be deleted. If set tofalse, it proceeds to delete the stale branches.
Automating with Azure DevOps Pipeline
Next, we define an Azure DevOps pipeline to run these scripts on a schedule. Here’s the azure-pipelines.yml file:
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'
env:
ACCESS_TOKEN: $(ACCESS_TOKEN)
- pwsh: .\delete-stale-branches.ps1
displayName: 'Delete stale branches'
env:
ACCESS_TOKEN: $(ACCESS_TOKEN)
Explanation
- Name and Schedule:
- The pipeline is named “Delete Branches”.
- It is scheduled to run at midnight every day.
- Pool:
- Uses the
ubuntu-latestimage.
- Uses the
- Steps:
- Install Azure DevOps CLI:
- Adds the Azure DevOps extension to the CLI.
- Checkout:
- Checks out the repository, ensuring a clean workspace.
- Login:
- Logs into Azure DevOps using a personal access token (PAT).
- Delete Merged Branches:
- Runs the
delete-pull-request-completed-branches.ps1script to delete branches that have been merged.
- Runs the
- Delete Stale Branches:
- Runs the
delete-stale-branches.ps1script to delete branches that are considered stale.
- Runs the
- Install Azure DevOps CLI:
Conclusion
By setting up these scripts and pipeline, you can automate the cleanup of both merged and stale branches in your Azure DevOps repository. This ensures that your repository remains clean and manageable, reducing clutter and making it easier to navigate.
Make sure to securely store your personal access token (PAT) in the Azure DevOps pipeline library as a secret variable, and adjust the paths in the pipeline YAML if your scripts are located elsewhere.
Happy automating!
