Introduction
As software projects evolve, Git repositories can become cluttered with outdated or redundant branches. This clutter makes repository navigation cumbersome and can introduce confusion or errors in the development process. Automating the cleanup of these branches helps maintain an organized and efficient development environment.
In this guide, we’ll walk through setting up a TypeScript script that automatically deletes old, unnecessary branches in Azure DevOps. We’ll cover the essential steps, focusing on the implementation and automation of the cleanup process.
Table of Contents
Open Table of Contents
- Why Automate Branch Cleanup?
- Prerequisites
- Project Setup
- Configuration File
- Implementing the TypeScript Script
- 1. Loading Environment Variables
- 2. Defining the Configuration Interface
- 3. Setting Constants
- 4. Getting the Git API Client
- 5. Loading the Configuration
- 6. Retrieving Repositories and Branches
- 7. Determining the Last Commit Date
- 8. Checking if a Branch is Merged
- 9. Deleting a Branch
- 10. Compiling the Exclusion List
- 11. Cleaning Up Branches
- Automating with Azure DevOps Pipeline
- Conclusion
- Additional Resources
- Thank You
Why Automate Branch Cleanup?
Automating branch cleanup is essential for several reasons:
- Reduce Clutter: Keeps the repository clean, making it easier for developers to navigate.
- Improve Performance: Enhances CI/CD pipeline performance by reducing overhead.
- Prevent Confusion: Minimizes the risk of developers working on or merging outdated branches.
- Enhance Security: Removes obsolete branches that may contain vulnerabilities.
Prerequisites
Ensure you have the following before starting:
- Azure DevOps Account: Access to your organization’s Azure DevOps instance.
- Personal Access Token (PAT): A PAT with permissions to access and manage repositories.
- Node.js and npm: Installed on your machine (Node.js version 14 or later).
- TypeScript: Installed globally (
npm install -g typescript
). - Azure DevOps Node API Package: Install via
npm install azure-devops-node-api
. - dotenv Package: Install via
npm install dotenv
.
Project Setup
-
Create a New Directory: Initialize a new Node.js project.
mkdir azure-devops-branch-cleanup cd azure-devops-branch-cleanup npm init -y
-
Install Dependencies:
npm install @azure/identity @microsoft/microsoft-graph-client azure-devops-node-api dayjs dotenv npm install --save-dev typescript @types/node
Configuration File
Create a config.json
file in your project root to specify branches that should be excluded from deletion:
{
"globalExcludes": ["master", "develop", "main", "release"],
"repositoryExcludes": {
"your-repo-name": ["feature/important-branch"]
}
}
- globalExcludes: Branches excluded from deletion across all repositories.
- repositoryExcludes: Specific branches to exclude in specific repositories.
Implementing the TypeScript Script
Create a TypeScript file, e.g., cleanup.ts
, and implement the following steps:
1. Loading Environment Variables
Use the dotenv
package to load environment variables.
import * as dotenv from "dotenv";
dotenv.config();
const isDryRun = process.env.DryRun === "true";
Environment Variables Required:
AZURE_DEVOPS_URL
: Your Azure DevOps organization URL.AZURE_DEVOPS_PAT
: Your Personal Access Token.AZURE_DEVOPS_PROJECT
: Your project name.DryRun
: Set to"true"
for dry-run mode (no actual deletions).
2. Defining the Configuration Interface
Define an interface to ensure type safety.
interface Config {
globalExcludes: string[];
repositoryExcludes: {
[repoName: string]: string[];
};
}
3. Setting Constants
Define constants used in the script.
const DAYS_90_MS = 90 * 24 * 60 * 60 * 1000; // 90 days in milliseconds
4. Getting the Git API Client
Authenticate and obtain the Git API client.
import * as azdev from "azure-devops-node-api";
import * as GitApi from "azure-devops-node-api/GitApi";
async function getGitApi(): Promise<GitApi.IGitApi> {
const orgUrl = process.env.AZURE_DEVOPS_URL;
const token = process.env.AZURE_DEVOPS_PAT;
if (!orgUrl || !token) {
throw new Error(
"Azure DevOps URL or PAT is not set in environment variables."
);
}
const authHandler = azdev.getPersonalAccessTokenHandler(token);
const connection = new azdev.WebApi(orgUrl, authHandler);
return await connection.getGitApi();
}
5. Loading the Configuration
Load the config.json
file.
import * as fs from "fs";
import * as path from "path";
function loadConfig(): Config {
const configPath = path.join(__dirname, "config.json");
const configContent = fs.readFileSync(configPath, "utf-8");
return JSON.parse(configContent) as Config;
}
6. Retrieving Repositories and Branches
Get the list of repositories and branches.
import * as GitInterfaces from "azure-devops-node-api/interfaces/GitInterfaces";
async function getRepositories(
gitApi: GitApi.IGitApi,
project: string
): Promise<GitInterfaces.GitRepository[]> {
return await gitApi.getRepositories(project);
}
async function getBranches(
gitApi: GitApi.IGitApi,
project: string,
repoId: string
): Promise<GitInterfaces.GitRef[]> {
const branches = await gitApi.getRefs(repoId, project);
return branches.filter(b => b.name.startsWith("refs/heads/"));
}
7. Determining the Last Commit Date
Get the date of the last commit on a branch.
async function getLastCommitDate(
gitApi: GitApi.IGitApi,
project: string,
repoId: string,
branchName: string
): Promise<Date | null> {
const commits = await gitApi.getCommits(
repoId,
{ itemVersion: { version: branchName } },
project,
undefined,
1
);
if (commits.length > 0) {
const commitDate = commits[0].committer.date || commits[0].author.date;
return new Date(commitDate);
}
return null;
}
8. Checking if a Branch is Merged
Check if a branch is merged into any of the target branches.
import { GitVersionType } from "azure-devops-node-api/interfaces/GitInterfaces";
async function isBranchMerged(
gitApi: GitApi.IGitApi,
project: string,
repoId: string,
branch: string,
targetBranches: string[]
): Promise<boolean> {
for (const targetBranch of targetBranches) {
const diff = await gitApi.getCommitDiffs(
repoId,
project,
true,
1,
undefined,
{ baseVersionType: GitVersionType.Branch, baseVersion: branch },
{ targetVersionType: GitVersionType.Branch, targetVersion: targetBranch }
);
if (diff && diff.aheadCount === 0) {
return true;
}
}
return false;
}
9. Deleting a Branch
Delete the branch if it meets the criteria.
async function deleteBranch(
gitApi: GitApi.IGitApi,
project: string,
repoId: string,
branch: GitInterfaces.GitRef
): Promise<void> {
if (!isDryRun) {
if (branch.isLocked) {
await gitApi.updateRef(
{ name: branch.name, isLocked: false },
repoId,
"",
project
);
}
await gitApi.updateRefs(
[
{
name: branch.name,
newObjectId: "0000000000000000000000000000000000000000",
oldObjectId: branch.objectId,
},
],
repoId,
"",
project
);
}
console.log(`Deleted branch: ${branch.name} (Dry Run: ${isDryRun})`);
}
10. Compiling the Exclusion List
Combine global and repository-specific exclusions.
function getExclusionList(config: Config, repoName: string): string[] {
const globalExcludes = config.globalExcludes || [];
const repoSpecificExcludes = config.repositoryExcludes[repoName] || [];
return [...new Set([...globalExcludes, ...repoSpecificExcludes])];
}
11. Cleaning Up Branches
Main function orchestrating the cleanup.
async function cleanUpBranches(): Promise<void> {
const project = process.env.AZURE_DEVOPS_PROJECT;
if (!project) {
throw new Error(
"Azure DevOps project name is not set in environment variables."
);
}
const config = loadConfig();
const now = new Date();
const gitApi = await getGitApi();
const repositories = await getRepositories(gitApi, project);
for (const repo of repositories) {
console.log(`Processing repository: ${repo.name}`);
const excludeBranches = getExclusionList(config, repo.name);
const branches = await getBranches(gitApi, project, repo.id);
for (const branch of branches) {
const branchName = branch.name.replace("refs/heads/", "");
if (excludeBranches.includes(branchName)) {
console.log(`Skipping excluded branch: ${branchName}`);
continue;
}
const lastCommitDate = await getLastCommitDate(
gitApi,
project,
repo.id,
branchName
);
if (
!lastCommitDate ||
now.getTime() - lastCommitDate.getTime() < DAYS_90_MS
) {
console.log(`Branch is recent or active: ${branchName}`);
continue;
}
const isMerged = await isBranchMerged(
gitApi,
project,
repo.id,
branchName,
config.globalExcludes
);
if (isMerged) {
await deleteBranch(gitApi, project, repo.id, branch);
} else {
console.log(`Branch is not merged: ${branchName}`);
}
}
}
}
cleanUpBranches().catch(err => {
console.error("An error occurred:", err);
});
Automating with Azure DevOps Pipeline
To automate the script execution, set up an Azure DevOps Pipeline.
-
Create a Variable Group:
- Navigate to Pipelines > Library in Azure DevOps.
- Click “Variable groups” > “Add variable group”.
- Name the group, e.g.,
az-devops
. - Add the variables:
AZURE_DEVOPS_URL
AZURE_DEVOPS_PAT
(set as secret)AZURE_DEVOPS_PROJECT
- Save the variable group.
-
Create the Pipeline YAML File:
Create a
azure-pipelines.yml
file in your repository:trigger: none schedules: - cron: "0 0 * * 0" # Runs every Sunday at 00:00 displayName: "Weekly Branch Cleanup" branches: include: - main always: true batch: false pool: vmImage: ubuntu-latest variables: - group: az-devops steps: - task: NodeTool@0 inputs: versionSpec: "14.x" displayName: "Install Node.js" - script: | npm ci npx ts-node cleanup.ts displayName: "Run Branch Cleanup Script" env: AZURE_DEVOPS_URL: $(AZURE_DEVOPS_URL) AZURE_DEVOPS_PAT: $(AZURE_DEVOPS_PAT) AZURE_DEVOPS_PROJECT: $(AZURE_DEVOPS_PROJECT)
- Notes:
- Replace
cleanup.ts
with the path to your script. - Ensure the pipeline has access to the variable group.
- Replace
- Notes:
Conclusion
Automating branch cleanup ensures your repositories remain organized, improving developer productivity and reducing potential errors. By following this guide, you can set up a script to automatically identify and delete old, unused branches in Azure DevOps, and schedule it using Azure Pipelines for regular maintenance.
Benefits:
- Efficiency: Saves time and resources.
- Consistency: Maintains a consistent repository state.
- Scalability: Easily extends to multiple projects and repositories.
Additional Resources
- Full Working Source Code: drunkcoding public code
- Azure DevOps Node API Documentation: Git API Reference
- Azure DevOps REST API Reference: Git Repositories
Note: Always test scripts in a controlled environment before deploying them in production. Ensure compliance with your organization’s policies and procedures.
Thank You
Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨
Steven GitHub