Skip to content

Cleanup Stale Branches #8

Cleanup Stale Branches

Cleanup Stale Branches #8

name: Cleanup Stale Branches
on:
schedule:
- cron: '0 8 * * 1' # Every Monday at 08:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'List stale branches without deleting'
type: boolean
default: true
permissions:
contents: write
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Delete stale branches
# To preserve your branch from cleanup, do ONE of:
# 1. Rename it under archive/: git branch -m my-branch archive/my-branch
# 2. Keep an open PR (including drafts) pointing to it
# 3. Add the branch pattern to PROTECTED below
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DRY_RUN="${{ github.event_name == 'schedule' && 'true' || inputs.dry_run }}"
COPILOT_DAYS=90
DEFAULT_DAYS=180
COPILOT_CUTOFF=$(date -d "$COPILOT_DAYS days ago" +%s)
DEFAULT_CUTOFF=$(date -d "$DEFAULT_DAYS days ago" +%s)
PROTECTED='^(main|master|develop|release/|hotfix/|archive/|[0-9]+\.[0-9]+-stable|preview-)'
echo "Stale threshold: copilot/* = $COPILOT_DAYS days, others = $DEFAULT_DAYS days | Dry run: $DRY_RUN"
git branch -r --format='%(refname:short) %(committerdate:unix)' | while read -r REF DATE; do
BRANCH="${REF#origin/}"
[ "$BRANCH" = "HEAD" ] && continue
echo "$BRANCH" | grep -qE "$PROTECTED" && continue
# copilot branches: 90 days, all others: 180 days
if echo "$BRANCH" | grep -q '^copilot'; then
[ "$DATE" -ge "$COPILOT_CUTOFF" ] 2>/dev/null && continue
else
[ "$DATE" -ge "$DEFAULT_CUTOFF" ] 2>/dev/null && continue
fi
# Skip branches with open PRs (including drafts)
PR_COUNT=$(gh pr list --head "$BRANCH" --state open --json number --jq 'length' 2>/dev/null || echo "0")
if [ "$PR_COUNT" -gt 0 ]; then
echo "[skipped] $BRANCH (has open PR)"
continue
fi
LAST=$(date -d "@$DATE" +%Y-%m-%d)
if [ "$DRY_RUN" = "true" ]; then
echo "[stale] $BRANCH (last commit: $LAST)"
else
echo "Deleting $BRANCH (last commit: $LAST)"
git push origin --delete "$BRANCH" || echo " Failed to delete $BRANCH"
fi
done