Files
get-shit-done/.github/workflows/branch-cleanup.yml
Tom Boucher 553d9db56e ci: upgrade GitHub Actions to Node 22+ runtimes (#2128)
- actions/checkout v4.2.2 → v6.0.2 (pr-gate, auto-branch)
- actions/github-script v7.0.1/v8 → v9.0.0 (all workflows)
- actions/stale v9.0.0 → v10.2.0

Eliminates Node.js 20 deprecation warnings. Node 20 actions
will be forced to Node 24 on June 2, 2026 and removed Sept 16, 2026.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 16:28:18 -04:00

124 lines
4.3 KiB
YAML

name: Branch Cleanup
on:
pull_request:
types: [closed]
schedule:
- cron: '0 4 * * 0' # Sunday 4am UTC — weekly orphan sweep
workflow_dispatch:
permissions:
contents: write
pull-requests: read
jobs:
# Runs immediately when a PR is merged — deletes the head branch.
# Belt-and-suspenders alongside the repo's delete_branch_on_merge setting,
# which handles web/API merges but may be bypassed by some CLI paths.
delete-merged-branch:
name: Delete merged PR branch
runs-on: ubuntu-latest
timeout-minutes: 2
if: github.event_name == 'pull_request' && github.event.pull_request.merged == true
steps:
- name: Delete head branch
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const branch = context.payload.pull_request.head.ref;
const protectedBranches = ['main', 'develop', 'release'];
if (protectedBranches.includes(branch)) {
core.info(`Skipping protected branch: ${branch}`);
return;
}
try {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${branch}`,
});
core.info(`Deleted branch: ${branch}`);
} catch (e) {
// 422 = branch already deleted (e.g. by delete_branch_on_merge setting)
if (e.status === 422) {
core.info(`Branch already deleted: ${branch}`);
} else {
throw e;
}
}
# Runs weekly to catch any orphaned branches whose PRs were merged
# before this workflow existed, or that slipped through edge cases.
sweep-orphaned-branches:
name: Weekly orphaned branch sweep
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
steps:
- name: Delete branches from merged PRs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const protectedBranches = new Set(['main', 'develop', 'release']);
const deleted = [];
const skipped = [];
// Paginate through all branches (100 per page)
let page = 1;
let allBranches = [];
while (true) {
const { data } = await github.rest.repos.listBranches({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
page,
});
allBranches = allBranches.concat(data);
if (data.length < 100) break;
page++;
}
core.info(`Scanning ${allBranches.length} branches...`);
for (const branch of allBranches) {
if (protectedBranches.has(branch.name)) continue;
// Find the most recent closed PR for this branch
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:${branch.name}`,
state: 'closed',
per_page: 1,
sort: 'updated',
direction: 'desc',
});
if (prs.length === 0 || !prs[0].merged_at) {
skipped.push(branch.name);
continue;
}
try {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${branch.name}`,
});
deleted.push(branch.name);
} catch (e) {
if (e.status !== 422) {
core.warning(`Failed to delete ${branch.name}: ${e.message}`);
}
}
}
const summary = [
`Deleted ${deleted.length} orphaned branch(es).`,
deleted.length > 0 ? ` Removed: ${deleted.join(', ')}` : '',
skipped.length > 0 ? ` Skipped (no merged PR): ${skipped.length} branch(es)` : '',
].filter(Boolean).join('\n');
core.info(summary);
await core.summary.addRaw(summary).write();