Backup Azure Repos to local disk
Using the Azure DevOps APIs it's easy to build a scheduled task to backup the repos in an Azure organization
The key points are:
- Create and authorize a user
- Query the apis for e.g. change date
- Git clone/git pull depending on backup state
This post is written as an Azure pipeline with PowerShell, but naturally you could do this with any scripting language. The goal here is to get a local copy of the repos, so we're using a self hosted agent to do the job.
The user and PAT
In order to use the APIs the user needs to be authenticated and have the appropriate permissions. With self hosted agents this is easy to handle by using the $(System.AccessToken)
variable
jobs:
- job: BackupRepos
displayName: 'Backup Azure DevOps Repositories'
steps:
- checkout: none
- pwsh: |
$organization = ${{ parameters.organization }}
$pat = "$(System.AccessToken)"
$localPath = ${{ parameters.localFilePath }}
$headerValue = "Basic " + [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(":" + $pat))
$header = "Authorization: " + $headerValue
$projectsUrl = "https://dev.azure.com/$organization/_apis/projects?api-version=7.1"
$projects = Invoke-RestMethod -Uri $projectsUrl -Method Get -Headers @{Authorization=($headerValue)}
See the authentication docs for more options.
Looping and cloning the repositories:
After you have the auth header in place, the rest is just a straightforward nested loop of projects, then repos, which we either pull or clone, depending on if they're already backed up or not:
foreach ($project in $projects.value) {
$projectName = $project.name
$reposUrl = "https://dev.azure.com/$organization/$projectName/_apis/git/repositories?api-version=7.1"
$repos = Invoke-RestMethod -Uri $reposUrl -Method Get -Headers @{Authorization=("$headerValue")}
foreach ($repo in $repos.value) {
$repoName = $repo.name
if ($repo.isDisabled) {
Write-Output "Skipping disabled repository: $repoName"
continue
}
$repoUrl = $repo.remoteUrl
$repoPath = Join-Path -Path $localPath -ChildPath "$projectName\$repoName"
# Check if the repository has any commits
$refsUrl = "https://dev.azure.com/$organization/$projectName/_apis/git/repositories/$repoName/refs?api-version=7.1"
$refs = Invoke-RestMethod -Uri $refsUrl -Method Get -Headers @{Authorization=("$headerValue")}
if ($refs.count -eq 0) {
Write-Output "Skipping empty repository: $repoName"
continue
}
try {
if (Test-Path -Path $repoPath) {
Write-Output "Updating repository: $repoName"
Set-Location -Path $repoPath
git -c http.extraheader=$header pull --prune
Set-Location -Path $localPath
} else {
Write-Output "Cloning repository: $repoName"
git -c http.extraheader=$header clone $repoUrl $repoPath
}
} catch {
$errorMessage = "Failed to process repository: $repoName. Error: $_"
Write-Warning $errorMessage
}
}
}
And there we go - a process for getting a backup of all the active, non-empty Azure repos grouped by project. The full pipeline is available here. This can be very useful for e.g. SBOM generation, which we'll look into next.
Thoughts, comments? Send me an email!