If you’ve ever had to clean up dozens of deleted repositories from your Azure DevOps Recycle Bin, you know it’s a painful manual process. Click, confirm, wait, repeat. When you’re dealing with 50+ repositories, this gets old fast. I wrote a Python script to handle this in bulk using the Azure DevOps REST API.

The complete script is available on GitHub: https://github.com/Timohone/azure-devops-repo-recycle-bin-cleaner/

The Problem

Azure DevOps has a Recycle Bin for deleted repositories. That’s great for recovering accidentally deleted repos, but when you actually want to permanently delete multiple repositories, there’s no bulk action in the UI. You have to permanently delete each one individually through the REST API.

What This Script Does

The script takes a JSON file containing deleted repository information and permanently deletes each repository from the Recycle Bin using the Azure DevOps REST API. It handles authentication, error handling, and provides clear feedback on the deletion progress.

You get a summary at the end showing how many repositories were successfully deleted and how many failed. The script is interactive and prompts you for all the necessary configuration details, so you don’t need to hardcode anything.

How It Works

The script uses the Azure DevOps REST API’s git recycleBin endpoint. Here’s the core deletion function:

def delete_repository(base_url, organization, project, repository_id, api_version, headers, auth):
"""
Permanently delete a repository from the Azure DevOps Recycle Bin.
"""
# Construct the URL
url = f"{base_url}/{organization}/{project}/_apis/git/recycleBin/repositories/{repository_id}?api-version={api_version}"
try:
response = requests.delete(url, headers=headers, auth=auth)
if response.status_code == 204:
print(f"✅ Successfully deleted repository ID: {repository_id}")
return True
else:
print(f"❌ Failed to delete repository ID: {repository_id}. Status Code: {response.status_code}. Response: {response.text}")
return False
except Exception as e:
print(f"⚠️ Error deleting repository ID: {repository_id}. Error: {str(e)}")
return False

The API returns a 204 status code for successful deletions. Anything else indicates a problem, and the script logs the error details so you can investigate.

For authentication, Azure DevOps uses Basic Auth with your Personal Access Token (PAT) as the password:

headers = {
'Content-Type': 'application/json'
}
auth = ('', pat) # Empty username, PAT as password

The script loads repository data from a JSON file. You can get this data by calling the Azure DevOps API to list deleted repositories.

def load_json(file_path):
"""
Load JSON data from a file.
"""
with open(file_path, 'r') as f:
return json.load(f)

The JSON structure should have a value array containing repository objects with at least an id field:

{
"count": 2,
"value": [
{
"id": "12345678-1234-1234-1234-123456789012",
"name": "old-repo-1"
},
{
"id": "87654321-4321-4321-4321-210987654321",
"name": "old-repo-2"
}
]
}

Running the Script

The script prompts for everything it needs:

base_url = input("🔗 Enter Azure DevOps Base URL (e.g., https://dev.azure.com): ").strip()
organization = input("🏢 Enter your Organization Name: ").strip()
project = input("📁 Enter your Project Name: ").strip()
api_version = input("🔍 Enter API Version (e.g., 7.1-preview.1): ").strip()
json_file_path = input("📄 Enter the path to your JSON file (e.g., deleted_repos.json): ").strip()
print("\n🔐 Enter your Azure DevOps Personal Access Token (PAT).")
print(" Note: Your PAT will not be displayed as you type.")
pat = getpass("PAT: ").strip()

The PAT input uses getpass so your token isn’t displayed on screen. Basic security practice.

Once you provide all the inputs, the script iterates through each repository in the JSON file and deletes it:

for repo in repositories:
repo_id = repo.get('id')
repo_name = repo.get('name')
if not repo_id:
print(f"⚠️ Repository name '{repo_name}' does not have an 'id'. Skipping.")
failure_count += 1
continue
success = delete_repository(base_url, organization, project, repo_id, api_version, headers=headers, auth=auth)
if success:
success_count += 1
else:
failure_count += 1

At the end, you get a summary:

📋 Deletion Process Completed.
✅ Successfully deleted: 48
❌ Failed to delete: 2

Getting Your Repository Data

To get the JSON file with deleted repositories, you can use the Azure DevOps REST API to query the Recycle Bin:

Terminal window
GET https://dev.azure.com/{organization}/{project}/_apis/git/recycleBin/repositories?api-version=7.1-preview.1

Save the response to a JSON file and you’re ready to run the deletion script.

Alternatively, if you already have a list of repository IDs from another source, you can manually construct the JSON file with the structure shown above.

Get the Script

Clone the repository and try it out:

Terminal window
git clone https://github.com/Timohone azure-devops-repo-recycle-bin-cleaner/
cd azdo-recycle-bin-cleanup
pip install requests
python delete_repositories.py

Check the README for detailed instructions on creating your PAT and preparing the JSON input file.

Bottom Line

The code is simple enough to modify for your specific needs. Change the authentication method, add filtering logic, integrate it with your existing automation. The foundation is there.

Full source available at https://github.com/Timohone/azure-devops-repo-recycle-bin-cleaner/.