Repository navigation
feat: daily automated branch cleanup workflow - #11
Conversation
Co-authored-by: tassan <7563760+tassan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a scheduled GitHub Actions workflow to automatically delete branches whose commits are fully merged into the repository’s default branch, reducing long-lived “dangling” branches over time.
Changes:
- Introduces a daily (03:00 UTC) + manual (
workflow_dispatch) workflow that enumerates all branches via pagination. - Resolves the repo default branch via the GitHub API and compares each branch against it before deletion.
- Skips a small allowlist of branch names and logs/continues on compare API failures.
| if (compare.ahead_by === 0) { | ||
| core.info(`Deleting merged branch: ${branch.name}`); | ||
| await github.rest.git.deleteRef({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| ref: `heads/${branch.name}`, | ||
| }); |
There was a problem hiding this comment.
Branch deletion errors aren’t caught. If git.deleteRef fails (e.g., branch protection rules, insufficient permissions, transient API errors), the whole job will fail mid-loop and leave remaining branches unprocessed. Wrap the delete call in a try/catch (similar to the compare call) and log+continue, and/or skip branches where branch.protected === true instead of relying only on a name allowlist.
| const protectedBranches = ['main', 'master', 'develop']; | ||
|
|
||
| // Resolve the repository's default branch dynamically | ||
| const { data: repo } = await github.rest.repos.get({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| }); | ||
| const defaultBranch = repo.default_branch; | ||
| core.info(`Default branch: ${defaultBranch}`); | ||
|
|
||
| // Paginate through all branches | ||
| const branches = await github.paginate(github.rest.repos.listBranches, { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| for (const branch of branches) { | ||
| if (protectedBranches.includes(branch.name)) { | ||
| core.info(`Skipping protected branch: ${branch.name}`); | ||
| continue; |
There was a problem hiding this comment.
The workflow can delete the repository’s default branch if it isn’t named one of main|master|develop. repos.listBranches includes the default branch, and comparing ${defaultBranch}...${branch.name} for the default branch yields ahead_by === 0, triggering deleteRef. Add an explicit guard to always skip branch.name === defaultBranch (or add defaultBranch to the protected list after resolving it).
Dangling merged branches accumulate over time with no automated cleanup. Adds a scheduled GitHub Actions workflow to delete branches that have been fully merged into the default branch.
Changes
.github/workflows/branch-cleanup.yml— new workflow triggered daily at 03:00 UTC (+workflow_dispatchfor on-demand runs)mainmain,master,developahead_by === 0(all commits already in default branch)GITHUB_TOKENwithcontents: write— no extra secrets requiredOriginal prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.