Runbook

Clean Up Merged Bitbucket Branches Automatically

Delete stale merged branches across all repositories in a Bitbucket workspace. Protect important branches with exclusion patterns and preview everything with dry-run mode.

What This Does

This runbook scans repositories in a Bitbucket workspace, identifies branches that have already been merged, and deletes them. Protected branches like main, master, and develop are automatically excluded via configurable patterns. Dry-run mode is enabled by default so you can preview exactly what will be removed before committing to any deletions.

Prerequisites

Quick Start

# Preview branches that would be deleted (dry-run)
./branch-cleanup.sh --workspace myworkspace --dry-run

# Target a specific repo with custom exclusions
./branch-cleanup.sh --workspace myworkspace --repo api-service \
  --exclude "main,master,develop,release/*"

# Execute the cleanup
./branch-cleanup.sh --workspace myworkspace --execute

Full Runbook Script

branch-cleanup.sh

#!/bin/bash
# Bitbucket Branch Cleanup Script
#
# Deletes merged branches across repositories in a workspace.
# Supports dry-run mode and exclusion patterns.
#
# Usage:
#   # Dry run to preview branches to delete
#   ./branch-cleanup.sh --workspace myworkspace --dry-run
#
#   # Delete merged branches (excluding protected branches)
#   ./branch-cleanup.sh --workspace myworkspace \
#                       --exclude "main,master,develop,release/*"
#
#   # Clean up a specific repository
#   ./branch-cleanup.sh --workspace myworkspace --repo myrepo
#
# Requirements:
#   - atlassian-cli installed and configured

set -euo pipefail

# Configuration
WORKSPACE=""
REPO=""
PROFILE="default"
DRY_RUN=true
EXCLUDE_PATTERNS="main,master,develop"
CONCURRENCY=4

# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

log() {
    echo -e "${GREEN}[INFO]${NC} $*"
}

warn() {
    echo -e "${YELLOW}[WARN]${NC} $*"
}

error() {
    echo -e "${RED}[ERROR]${NC} $*" >&2
}

# Parse arguments
parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            --workspace)
                WORKSPACE="$2"
                shift 2
                ;;
            --repo)
                REPO="$2"
                shift 2
                ;;
            --profile)
                PROFILE="$2"
                shift 2
                ;;
            --exclude)
                EXCLUDE_PATTERNS="$2"
                shift 2
                ;;
            --execute)
                DRY_RUN=false
                shift
                ;;
            --dry-run)
                DRY_RUN=true
                shift
                ;;
            *)
                error "Unknown option: $1"
                exit 1
                ;;
        esac
    done

    if [ -z "$WORKSPACE" ]; then
        error "Workspace slug required (--workspace WORKSPACE)"
        exit 1
    fi
}

# Check if branch should be excluded
is_excluded() {
    local branch="$1"

    IFS=',' read -ra patterns <<< "$EXCLUDE_PATTERNS"
    for pattern in "${patterns[@]}"; do
        # Simple wildcard matching
        if [[ "$branch" == $pattern ]]; then
            return 0
        fi
    done

    return 1
}

# Get all repositories or specific repo
get_repos() {
    if [ -n "$REPO" ]; then
        echo "[\"$REPO\"]"
    else
        log "Fetching all repositories from workspace: $WORKSPACE"
        atlassian-cli bitbucket repo list \
            --profile "$PROFILE" \
            "$WORKSPACE" \
            --output json | jq -r '[.[].slug]'
    fi
}

# Get merged branches for a repository
get_merged_branches() {
    local repo="$1"

    log "Finding merged branches in $repo..."

    # Get all branches
    local branches
    branches=$(atlassian-cli bitbucket branch list \
        --profile "$PROFILE" \
        "$WORKSPACE" \
        "$repo" \
        --output json)

    # Filter for merged branches
    echo "$branches" | jq -r '.[] | select(.merge_strategies != null) | .name'
}

# Delete branch
delete_branch() {
    local repo="$1"
    local branch="$2"

    if [ "$DRY_RUN" = "true" ]; then
        warn "[DRY-RUN] Would delete branch: $repo/$branch"
        return
    fi

    log "Deleting branch: $repo/$branch"

    atlassian-cli bitbucket branch delete \
        --profile "$PROFILE" \
        "$WORKSPACE" \
        "$repo" \
        "$branch" || warn "Failed to delete: $branch"
}

# Process repository branches
process_repo() {
    local repo="$1"

    log "Processing repository: $repo"

    local branches
    branches=$(get_merged_branches "$repo")

    if [ -z "$branches" ]; then
        log "No merged branches found in $repo"
        return
    fi

    local deleted=0
    local skipped=0

    while IFS= read -r branch; do
        if is_excluded "$branch"; then
            log "Skipping protected branch: $branch"
            skipped=$((skipped + 1))
            continue
        fi

        delete_branch "$repo" "$branch"
        deleted=$((deleted + 1))

    done <<< "$branches"

    log "Repository $repo: $deleted deleted, $skipped skipped"
}

# Main execution
main() {
    parse_args "$@"

    log "Bitbucket Branch Cleanup"
    log "Workspace: $WORKSPACE | Profile: $PROFILE"

    if [ "$DRY_RUN" = "true" ]; then
        warn "DRY-RUN MODE: No branches will be deleted"
    fi

    log "Exclude patterns: $EXCLUDE_PATTERNS"

    local repos
    repos=$(get_repos)

    if [ "$(echo "$repos" | jq '. | length')" -eq 0 ]; then
        error "No repositories found"
        exit 1
    fi

    local total_repos
    total_repos=$(echo "$repos" | jq '. | length')
    log "Found $total_repos repositories to process"

    # Process each repository
    echo "$repos" | jq -r '.[]' | while IFS= read -r repo_name; do
        process_repo "$repo_name"
    done

    log "Branch cleanup complete"

    if [ "$DRY_RUN" = "true" ]; then
        warn "This was a dry run. Use --execute to actually delete branches."
    fi
}

main "$@"

How It Works

1

Configure workspace and exclusions. The script accepts --workspace (required), an optional --repo to target a single repository, and --exclude for a comma-separated list of branch patterns to protect. Defaults protect main, master, and develop.

2

Discover repositories. If no specific repo is given, the script calls bitbucket repo list to fetch every repository in the workspace. The results are parsed as JSON and iterated one by one.

3

Find merged branches. For each repository, bitbucket branch list retrieves all branches. The script filters for branches that have been merged, excluding any that match the protected patterns using bash wildcard matching.

4

Dry-run preview. By default, the script runs in dry-run mode and prints each branch that would be deleted without making changes. This lets you audit the cleanup before committing.

5

Execute deletion. Pass --execute to perform the actual branch deletions. The script reports a per-repository summary of how many branches were deleted and how many were skipped due to exclusion rules.

Related Runbooks

Copied!