Skip to content

Commit c1459fb

Browse files
Add automated cleanup workflow for stale lint/* branches (#280)
* Initial plan * Add cleanup workflow for lint branches Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * Fix code review issues: safer dry-run defaults, branch validation, and loop handling Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * Improve error handling and logging clarity Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * Use safer branch deletion and improved jq handling Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * Simplify jq filter and properly capture exit codes Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com>
1 parent 1dfbc07 commit c1459fb

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
name: "🧹 Cleanup Lint Branches"
2+
3+
# This workflow automatically cleans up lint/* branches that have been merged or closed.
4+
# It helps maintain a clean repository by removing branches created by the linter workflow
5+
# after their associated pull requests are no longer active.
6+
7+
on:
8+
# Run daily at 00:00 UTC
9+
schedule:
10+
- cron: '0 0 * * *'
11+
12+
# Allow manual triggering with optional dry-run mode
13+
workflow_dispatch:
14+
inputs:
15+
dry_run:
16+
description: 'Dry run mode (preview deletions without actually deleting)'
17+
required: false
18+
type: boolean
19+
default: true
20+
21+
permissions:
22+
contents: write
23+
pull-requests: read
24+
25+
jobs:
26+
cleanup:
27+
name: Clean up merged/closed lint branches
28+
runs-on: ubuntu-latest
29+
30+
steps:
31+
- name: Checkout repository
32+
uses: actions/checkout@v4
33+
with:
34+
fetch-depth: 0 # Fetch all branches and history
35+
token: ${{ secrets.GITHUB_TOKEN }}
36+
37+
- name: Setup GitHub CLI
38+
run: |
39+
# Verify gh CLI is available (pre-installed on ubuntu-latest)
40+
gh --version
41+
env:
42+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
44+
- name: Cleanup lint branches
45+
env:
46+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
# Default to true (dry-run mode) unless explicitly set to false
48+
# This ensures safe operation for both scheduled and manual triggers
49+
DRY_RUN: ${{ github.event.inputs.dry_run == 'false' && 'false' || 'true' }}
50+
BRANCH_AGE_DAYS: 7
51+
run: |
52+
set -e
53+
set -o pipefail
54+
55+
echo "🧹 Starting lint branch cleanup..."
56+
echo "Dry run mode: $DRY_RUN"
57+
echo "Branch age threshold for orphaned branches: $BRANCH_AGE_DAYS days"
58+
echo ""
59+
60+
# Get current date in seconds since epoch
61+
current_date=$(date +%s)
62+
age_threshold_seconds=$((BRANCH_AGE_DAYS * 86400))
63+
64+
# Track statistics
65+
total_branches=0
66+
deleted_branches=0
67+
skipped_branches=0
68+
error_branches=0
69+
70+
# Get all remote branches matching the pattern lint/style-fixes-*
71+
echo "📋 Fetching lint branches..."
72+
lint_branches=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/lint/style-fixes-*' | sed 's|^origin/||' || echo "")
73+
74+
if [ -z "$lint_branches" ]; then
75+
echo "✅ No lint branches found matching 'lint/style-fixes-*'"
76+
exit 0
77+
fi
78+
79+
echo "Found $(echo "$lint_branches" | wc -l) lint branches"
80+
echo ""
81+
82+
# Process each branch (using while read to handle special characters safely)
83+
while IFS= read -r branch; do
84+
[ -z "$branch" ] && continue
85+
total_branches=$((total_branches + 1))
86+
echo "🔍 Processing: $branch"
87+
88+
# Validate branch name matches expected pattern
89+
if [[ ! "$branch" =~ ^lint/style-fixes-[0-9]+$ ]]; then
90+
echo " ⚠️ Warning: Branch name doesn't match expected pattern, skipping"
91+
skipped_branches=$((skipped_branches + 1))
92+
echo ""
93+
continue
94+
fi
95+
96+
# Check if there's a PR for this branch
97+
pr_number=$(gh pr list --state all --head "$branch" --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
98+
99+
should_delete=false
100+
delete_reason=""
101+
102+
if [ -n "$pr_number" ]; then
103+
# PR exists, check its state
104+
echo " Found PR #$pr_number"
105+
106+
pr_state=$(gh pr view "$pr_number" --json state --jq '.state' 2>/dev/null || echo "")
107+
pr_merged=$(gh pr view "$pr_number" --json merged --jq '.merged' 2>/dev/null || echo "false")
108+
109+
if [ "$pr_merged" = "true" ]; then
110+
should_delete=true
111+
delete_reason="PR #$pr_number was merged"
112+
elif [ "$pr_state" = "CLOSED" ]; then
113+
should_delete=true
114+
delete_reason="PR #$pr_number was closed"
115+
else
116+
echo " ⏭️ Skipping: PR #$pr_number is still open"
117+
skipped_branches=$((skipped_branches + 1))
118+
fi
119+
else
120+
# No PR found - check if branch is old enough to delete
121+
echo " No associated PR found (orphaned branch)"
122+
123+
# Get the last commit date on this branch
124+
last_commit_date=$(git log -1 --format=%ct "origin/$branch" 2>/dev/null || echo "0")
125+
126+
if [ "$last_commit_date" != "0" ]; then
127+
branch_age_seconds=$((current_date - last_commit_date))
128+
branch_age_days=$((branch_age_seconds / 86400))
129+
130+
echo " Branch age: $branch_age_days days"
131+
132+
if [ $branch_age_seconds -gt $age_threshold_seconds ]; then
133+
should_delete=true
134+
delete_reason="Orphaned branch older than $BRANCH_AGE_DAYS days (age: $branch_age_days days)"
135+
else
136+
echo " ⏭️ Skipping: Orphaned branch is only $branch_age_days days old (threshold: $BRANCH_AGE_DAYS days)"
137+
skipped_branches=$((skipped_branches + 1))
138+
fi
139+
else
140+
echo " ⚠️ Warning: Could not determine branch age"
141+
skipped_branches=$((skipped_branches + 1))
142+
fi
143+
fi
144+
145+
# Delete the branch if appropriate
146+
if [ "$should_delete" = "true" ]; then
147+
if [ "$DRY_RUN" = "true" ]; then
148+
echo " 🔍 [DRY RUN] Would delete: $delete_reason"
149+
deleted_branches=$((deleted_branches + 1))
150+
else
151+
echo " 🗑️ Deleting: $delete_reason"
152+
# Use git push for safer deletion with validated branch name
153+
delete_error=$(git push origin --delete "$branch" 2>&1)
154+
exit_code=$?
155+
if [ $exit_code -eq 0 ]; then
156+
echo " ✅ Successfully deleted"
157+
deleted_branches=$((deleted_branches + 1))
158+
else
159+
echo " ❌ Failed to delete branch: $delete_error"
160+
error_branches=$((error_branches + 1))
161+
fi
162+
fi
163+
fi
164+
165+
echo ""
166+
done <<< "$lint_branches"
167+
168+
# Print summary
169+
echo "📊 Cleanup Summary"
170+
echo "=================="
171+
echo "Total lint branches processed: $total_branches"
172+
if [ "$DRY_RUN" = "true" ]; then
173+
echo "Branches that would be deleted: $deleted_branches"
174+
else
175+
echo "Branches deleted: $deleted_branches"
176+
fi
177+
echo "Branches skipped: $skipped_branches"
178+
if [ $error_branches -gt 0 ]; then
179+
echo "Branches with errors: $error_branches"
180+
fi
181+
echo ""
182+
183+
if [ "$DRY_RUN" = "true" ]; then
184+
echo "✅ Dry run completed - no branches were actually deleted"
185+
else
186+
echo "✅ Cleanup completed"
187+
fi

0 commit comments

Comments
 (0)