chore: add workflow to auto-delete branches on PR merge + bulk cleanup #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Cleanup Merged PR Branches | |
| on: | |
| # Auto-delete head branch whenever a PR is merged | |
| pull_request: | |
| types: [closed] | |
| # Manual trigger to bulk-delete all existing merged-PR branches | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| cleanup-on-merge: | |
| if: github.event_name == 'pull_request' && github.event.pull_request.merged == true | |
| name: Delete branch on PR merge | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Delete head branch | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const branch = context.payload.pull_request.head.ref; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // Never delete protected branches | |
| const protectedBranches = ['main', 'develop', 'master']; | |
| if (protectedBranches.includes(branch)) { | |
| console.log(`Skipping protected branch: ${branch}`); | |
| return; | |
| } | |
| try { | |
| await github.rest.git.deleteRef({ | |
| owner, | |
| repo, | |
| ref: `heads/${branch}`, | |
| }); | |
| console.log(`Deleted branch: ${branch}`); | |
| } catch (err) { | |
| if (err.status === 422) { | |
| console.log(`Branch already deleted: ${branch}`); | |
| } else { | |
| throw err; | |
| } | |
| } | |
| bulk-cleanup: | |
| if: github.event_name == 'workflow_dispatch' | |
| name: Bulk delete all merged-PR branches | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Delete branches for merged PRs | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const protectedBranches = new Set(['main', 'develop', 'master']); | |
| // Collect all head branches from merged PRs (paginate through all pages) | |
| const mergedBranches = new Set(); | |
| for await (const response of github.paginate.iterator( | |
| github.rest.pulls.list, | |
| { owner, repo, state: 'closed', per_page: 100 } | |
| )) { | |
| for (const pr of response.data) { | |
| if (pr.merged_at) { | |
| mergedBranches.add(pr.head.ref); | |
| } | |
| } | |
| } | |
| // Fetch all current branches | |
| const existingBranches = new Set(); | |
| for await (const response of github.paginate.iterator( | |
| github.rest.repos.listBranches, | |
| { owner, repo, per_page: 100 } | |
| )) { | |
| for (const branch of response.data) { | |
| existingBranches.add(branch.name); | |
| } | |
| } | |
| // Delete branches that are in merged PRs and still exist | |
| const toDelete = [...mergedBranches].filter( | |
| b => existingBranches.has(b) && !protectedBranches.has(b) | |
| ); | |
| console.log(`Found ${toDelete.length} branches to delete.`); | |
| let deleted = 0; | |
| let failed = 0; | |
| for (const branch of toDelete) { | |
| try { | |
| await github.rest.git.deleteRef({ | |
| owner, | |
| repo, | |
| ref: `heads/${branch}`, | |
| }); | |
| console.log(`Deleted: ${branch}`); | |
| deleted++; | |
| } catch (err) { | |
| if (err.status === 422) { | |
| console.log(`Already gone: ${branch}`); | |
| } else { | |
| console.error(`Failed to delete ${branch}: ${err.message}`); | |
| failed++; | |
| } | |
| } | |
| } | |
| console.log(`Done. Deleted: ${deleted}, Failed: ${failed}`); | |
| if (failed > 0) { | |
| throw new Error(`Failed to delete ${failed} branch(es). See logs above.`); | |
| } |