Skip to content

Commit 0863464

Browse files
committed
docs: add branch cleanup workflow and update contributing guide
- Add cleanup-stale-branches.yml workflow for automatic deletion of abandoned branches (>90 days old, no open PRs) - Document new branch lifecycle and retention policy in CONTRIBUTING.md - Protected branches (main, content) are never deleted - Creates audit issue after each cleanup run
1 parent 05b73dc commit 0863464

File tree

2 files changed

+153
-1
lines changed

2 files changed

+153
-1
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
name: Cleanup Stale Branches
2+
3+
on:
4+
schedule:
5+
# Run every Monday at 10:00 UTC
6+
- cron: "0 10 * * 1"
7+
workflow_dispatch: # Allow manual trigger
8+
9+
env:
10+
# Configuration constants for easy maintenance
11+
CUTOFF_DAYS: 90
12+
PROTECTED_BRANCHES: "main|content"
13+
14+
jobs:
15+
cleanup:
16+
name: Delete Stale Branches
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: write
20+
issues: write
21+
pull-requests: read
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0 # Get all history for accurate date calculation
27+
28+
- name: Get stale branches
29+
id: stale
30+
env:
31+
GH_TOKEN: ${{ github.token }}
32+
shell: bash
33+
run: |
34+
set -e
35+
36+
# Calculate cutoff timestamp
37+
CUTOFF=$(date -d "$CUTOFF_DAYS days ago" +%s)
38+
39+
echo "Finding branches older than $CUTOFF_DAYS days (before: $(date -d "@$CUTOFF" -Rfc 3339))"
40+
41+
# Find all remote branches except protected ones and HEAD
42+
STALE_BRANCHES=$(
43+
git for-each-ref --sort=-committerdate \
44+
--format='%(refname:short)|%(committerdate:unix)|%(authorname)' \
45+
refs/remotes/origin/ | \
46+
awk -F'|' -v cutoff="$CUTOFF" -v protected="$PROTECTED_BRANCHES" '
47+
# Skip origin/HEAD
48+
$1 !~ /^origin\/HEAD$/ &&
49+
# Skip protected branches (main, content, etc.) using env variable
50+
$1 ~ "^origin/(" protected ")$" {next}
51+
# Check if branch is older than cutoff
52+
$2 < cutoff {print $1}
53+
' | sed 's|^origin/||'
54+
)
55+
56+
echo "Found $(echo "$STALE_BRANCHES" | grep -c .) stale branch candidates"
57+
58+
# Get all open PR branches (with pagination)
59+
echo "Fetching open PR branches to preserve..."
60+
OPEN_PRS=$(
61+
gh pr list --limit 1000 --state open --json headRefName --jq '.[].headRefName'
62+
)
63+
64+
echo "Found $(echo "$OPEN_PRS" | grep -c .) open PR branches to preserve"
65+
66+
# Filter out branches with open PRs (using exact string matching)
67+
final=""
68+
for br in $STALE_BRANCHES; do
69+
# Use exact match with grep -F and -x (whole line)
70+
if echo "$OPEN_PRS" | grep -Fxq "$br"; then
71+
echo " Skipping: $br (has open PR)"
72+
else
73+
printf -v final '%s\n%s' "$final" "$br"
74+
fi
75+
done
76+
77+
# Output to GitHub Actions (handle empty case safely)
78+
printf 'branches<<EOF\n%s\nEOF\n' "$final" >> $GITHUB_OUTPUT
79+
80+
- name: Delete branches
81+
id: delete
82+
env:
83+
GH_TOKEN: ${{ github.token }}
84+
if: steps.stale.outputs.branches != ''
85+
shell: bash
86+
run: |
87+
BRANCHES_TO_DELETE="${{ steps.stale.outputs.branches }}"
88+
89+
if [ -z "$BRANCHES_TO_DELETE" ] || [ "$BRANCHES_TO_DELETE" = "EOF" ]; then
90+
echo "No branches to delete"
91+
echo "deleted_branches=" >> $GITHUB_OUTPUT
92+
exit 0
93+
fi
94+
95+
deleted=""
96+
for br in $BRANCHES_TO_DELETE; do
97+
echo " Deleting: $br"
98+
if git push origin --delete "$br" 2>/dev/null; then
99+
printf -v deleted '%s\n%s' "$deleted" "$br"
100+
else
101+
echo " Failed to delete: $br"
102+
fi
103+
done
104+
105+
printf 'deleted_branches<<EOF\n%s\nEOF\n' "$deleted" >> $GITHUB_OUTPUT
106+
107+
- name: Create cleanup issue (audit trail)
108+
if: steps.delete.outputs.deleted_branches != '' && steps.delete.outputs.deleted_branches != 'EOF'
109+
uses: actions/github-script@v8
110+
with:
111+
script: |
112+
const deletedBranches = "${{ steps.delete.outputs.deleted_branches }}".trim().split('\n').filter(b => b);
113+
114+
if (deletedBranches.length === 0) {
115+
console.log('No branches deleted, skipping issue creation');
116+
return;
117+
}
118+
119+
await github.rest.issues.create({
120+
owner: context.repo.owner,
121+
repo: context.repo.repo,
122+
title: '🧹 Automated Branch Cleanup Report',
123+
body: `## Deleted Stale Branches
124+
125+
The following branches (>${{ env.CUTOFF_DAYS }} days old, no open PRs) were automatically deleted:
126+
127+
${deletedBranches.map(b => `- \`${b}\``).join('\n')}
128+
129+
---
130+
**Configuration:**
131+
- Cutoff: ${{ env.CUTOFF_DAYS }} days
132+
- Protected branches: ${{ env.PROTECTED_BRANCHES }}
133+
134+
*This issue was automatically created by the cleanup-stale-branches workflow*`,
135+
labels: ['automation', 'maintenance']
136+
});

CONTRIBUTING.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ Thank you for your interest in contributing! This guide explains our two-branch
1515

1616
We use a **two-branch architecture** to separate code from generated content:
1717

18+
### Branch Lifecycle & Cleanup
19+
20+
**Automatic Cleanup:**
21+
22+
- Merged PR branches are automatically deleted via GitHub settings
23+
- Stale branches (>90 days old, no open PRs) are cleaned up weekly
24+
- Branches with open PRs are **never** automatically deleted
25+
26+
**Retention Policy:**
27+
28+
- Active PR branches: Preserved until PR closes/merges
29+
- Merged PR branches: Deleted immediately on merge
30+
- Abandoned branches (>90 days): Deleted via scheduled automation
31+
- Protected branches (main, content): Never deleted
32+
1833
### `main` branch
1934

2035
**Purpose**: Source code, scripts, and configuration
@@ -416,7 +431,8 @@ Before submitting PR:
416431

417432
### After PR is Merged
418433

419-
- Feature branch is automatically deleted
434+
- Feature branch is automatically deleted (via GitHub auto-delete setting)
435+
- Stale branches (>90 days old, no open PRs) are cleaned up weekly via [automation](.github/workflows/cleanup-stale-branches.yml)
420436
- Changes deploy to staging automatically
421437
- Production deployment is manual (maintainers only)
422438

0 commit comments

Comments
 (0)