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 web interface.
For cleanup operations after migrations, reorganizations, or testing, this manual process is a time sink. If you have 100 repositories to permanently delete, you’re looking at a significant chunk of time just clicking through dialogs.
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 FalseThe 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 passwordThe script loads repository data from a JSON file. You can get this data by calling the Azure DevOps API to list deleted repositories, or by exporting it from the Azure DevOps UI if you’ve got the data in another format:
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 += 1At the end, you get a summary:
📋 Deletion Process Completed.✅ Successfully deleted: 48❌ Failed to delete: 2Getting Your Repository Data
To get the JSON file with deleted repositories, you can use the Azure DevOps REST API to query the Recycle Bin:
GET https://dev.azure.com/{organization}/{project}/_apis/git/recycleBin/repositories?api-version=7.1-preview.1Save 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.
Error Handling
The script includes error handling at multiple levels. It validates that all required inputs are provided, checks that the JSON file exists and is valid, and catches exceptions during the deletion process.
If a repository deletion fails, the script logs the error but continues processing the remaining repositories. This prevents one bad repository ID from killing the entire cleanup operation.
Security Considerations
Never hardcode your PAT in the script. The interactive input approach keeps your token out of the source code and command history. If you need to run this in an automated pipeline, use environment variables or a secure secret store instead.
The PAT needs appropriate permissions for the operation. Specifically, it needs the “Code (Read & Write)” permission to delete repositories from the Recycle Bin.
Use Cases
This script is particularly useful for:
- Post-migration cleanup when you’ve moved repositories to a new organization or project
- Removing test repositories created during development or proof-of-concept work
- Cleanup after organizational restructuring where old repository structures need to be permanently removed
- Compliance requirements where deleted repositories need to be purged after a retention period
Extending the Script
You could enhance this script in several ways. Adding dry-run mode would let you see what would be deleted without actually deleting anything. Logging to a file would create an audit trail. Adding filters to target specific repositories by name pattern or deletion date would make it more selective.
Another useful addition would be interactive confirmation. Instead of deleting everything in the JSON file automatically, prompt for confirmation after showing the list of repositories to be deleted.
Why Python
Python makes REST API interactions straightforward with the requests library. The interactive input handling with getpass is built-in. JSON parsing is native. It’s a good fit for this kind of operations script.
You could write this in PowerShell and it would work fine too. I went with Python because it’s what I had open at the time and the requests library makes HTTP operations clean.
Get the Script
Clone the repository and try it out:
git clone https://github.com/Timohone azure-devops-repo-recycle-bin-cleaner/cd azdo-recycle-bin-cleanuppip install requestspython delete_repositories.pyCheck the README for detailed instructions on creating your PAT and preparing the JSON input file.
Bottom Line
Manual cleanup of Azure DevOps Recycle Bin repositories doesn’t scale. This script automates the process and saves significant time when you need to permanently delete multiple repositories. It’s straightforward, includes proper error handling, and gives you clear feedback on what succeeded and what failed.
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/.