While Azure Devops is widely used, Microsoft’s backup solutions are surprisingly thin. With people depending on it, individuals and enterprises alike, you’d expect a bit more.
There are various tools around, but here’s my version in the form of a Powershell script. What it does is:
- Connects to a specific Azure Devops project and repo.
- Lists all branches, downloads them using git and zips them.
- The zip, one for every branch, is named
Backup_yyyy-MM-dd_branch.zip.
Prerequisites are not much, but:
So here’s the script:
#
# Source: DotJim blog (https://dandraka.com)
# Jim Andrakakis, October 2024
# Updated September 2025, fix for projects and orgs containing spaces
#
# BackupBranches.ps1
param (
[string]$organization = "MYORG",
[string]$project = "MYPROJECT",
[string]$repository = "MYREPO",
[string]$backupFolder = "C:\Temp\DevOpsBranches",
[string]$branchFilter = "" # leave empty for all branches
)
Clear-Host
$ErrorActionPreference='Stop'
$pat = Read-Host -MaskInput -Prompt "Enter Personal Access Token for $($env:USERNAME) and $($project)/$($repository)"
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
$tempFolder = Join-Path $backupFolder $repository
$repoNoSpace = $repository.Replace(' ','%20')
$projNoSpace = $project.Replace(' ','%20')
Write-Host "[$([datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))] Starting, output directory is $tempFolder"
# Ensure temp folder exists
if (-not (Test-Path -Path $tempFolder)) {
New-Item -Path $tempFolder -ItemType Directory | Out-Null
}
# API URL for branches
$branchesApiUrl = "https://dev.azure.com/$organization/$projNoSpace/_apis/git/repositories/$repoNoSpace/refs?filter=heads/&api-version=6.0"
# Get all branches from the repository
$response = Invoke-RestMethod -Uri $branchesApiUrl -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)}
$branchList = $response.value | Sort-Object -Property name
# Iterate through each branch
foreach ($branch in $branchList) {
try {
$branchName = $branch.name -replace "refs/heads/", ""
# branch filter, if any
if (-not ([string]::IsNullOrWhiteSpace($branchFilter)) -and ($branchName -notlike "*$branchFilter*")) {
continue
}
# Define the folder for the branch
$branchNameStrilized = "$($branchName.Replace('/','_').Replace(' ','_'))"
$branchFolder = "$tempFolder\$branchNameStrilized"
# Remove the folder if it exists from previous runs
if (Test-Path -Path $branchFolder) {
Remove-Item -Recurse -Force $branchFolder
}
# Clone the specific branch
$gitUrl = "https://dev.azure.com/$organization/$projNoSpace/_git/$repoNoSpace"
Write-Host "Cloning branch '$branchName' from $gitUrl to $branchFolder"
$gitResp = [string] (& git clone --branch $branchName --single-branch $gitUrl $branchFolder 2>&1)
if ($gitResp -like "*fatal*") {
Write-Error "Error cloning branch '$branchName': $gitResp"
}
# Zip the branch folder
$backupDate = [datetime]::Now.ToString('yyyy-MM-dd')
$zipFilePath = "$tempFolder\Backup_$($backupDate)_$($branchNameStrilized).zip"
if (Test-Path $zipFilePath) {
Remove-Item $zipFilePath
}
Compress-Archive -CompressionLevel Fastest -Path "$branchFolder\*" -DestinationPath $zipFilePath
Write-Host "Branch '$branchName' zipped to $zipFilePath"
# Clean up branch folder after zipping
Remove-Item -Recurse -Force $branchFolder
}
catch {
Write-Warning $_
}
}
Write-Host "[$([datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))] Finished, $($response.value.Count) branches processed."
Usage example:
BackupBranches.ps1 -organization 'BIGBANK' -project 'KYCAML' -repository 'KYCAMLapiV2' -backupFolder '\\backupfileserver\codebackups\'