Skip to content

ci: delete a PR's head branch after it merges, with an allowlist - #15

Merged
rishitank merged 1 commit into
masterfrom
ci/branch-cleanup
Oct 9, 2026
Merged

rishitank merged 1 commit into
masterfrom
ci/branch-cleanup

Conversation

@rishitank

Copy link
Copy Markdown
Owner

Why

Merged PR branches pile up. GitHub has no account-wide setting to delete them: "Automatically delete head branches" is a per-repository toggle, and it deletes every merged head branch with no exceptions. Private repositories on the Free plan can't protect branches, so that toggle could delete a long-lived branch such as develop the first time it is merged as a PR head.

This workflow does the cleanup with an explicit allowlist instead.

What changed

  • New .github/workflows/branch-cleanup.yml. Self-contained, no reusable workflow (rishitank/.github main is ruleset-blocked, so a shared copy can't land yet).
    • Trigger: pull_request closed. The one job runs only when the PR was merged and its head is in this repository (never a fork's branch).
    • Keeps the head branch, with a ::notice::, when it is the default branch, or one of main, master, develop, staging, test, gh-pages, deploy-*, release/*, or when any open PR still uses it as its base.
    • Otherwise deletes it with gh api -X DELETE repos/$REPO/git/refs/heads/<ref>, the ref URL-encoded with jq @uri. "Reference does not exist" (422) or a 404 counts as success, since the branch is already gone.
    • Permissions: workflow default {}; the job gets contents: write (delete the ref) and pull-requests: read (the open-PR check). timeout-minutes: 2.
    • No ${{ }} in run:. Every event value reaches the shell through env: and is quoted.
  • Checked locally: actionlint and shellcheck are clean. The script was run against a stub gh for the default branch, each allowlisted name, a branch with open child PRs, a ref containing /, a space and #, an already-deleted ref (422 and 404), and a 403, which fails the job.

Proof on this repo

This repo is the live test: merging this very PR should delete ci/branch-cleanup (a ref with a /, so the URL-encoding is exercised too).

🤖 Generated with Claude Code

https://claude.ai/code/session_012rYJxu978iUSGFZ1UuqTsi

GitHub has no account-wide setting for this, the per-repo toggle deletes
every merged head branch, and Free-plan private repos cannot protect
long-lived branches such as develop. This self-contained workflow runs
on merged same-repo PRs and deletes the head branch unless it is the
default branch, one of main/master/develop/staging/test/gh-pages/
deploy-*/release/*, or the base of another open PR. A branch that is
already gone counts as success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rYJxu978iUSGFZ1UuqTsi
@coderabbitai

coderabbitai Bot commented Oct 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 57 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 5e5268e9-99b2-40b8-9c57-109358c9faa2
📥 Commits

Reviewing files that changed from the base of the PR and between 60f379f and 6ac84d1.

📒 Files selected for processing (1)
  • .github/workflows/branch-cleanup.yml
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

🧪 Runtime gate

✅ Runtime crawl: no runtime errors

1 page(s) opened in a real browser (desktop + phone), safe controls exercised, forms tried with dummy data, links followed client-side, missing-route probe run. 38s.

Coverage: 1/1 routes · 0/0 controls operated · 0/0 forms submitted · 29.3% of the app's own JavaScript executed

Against the default branch: routes ±0, controls ±0, JS ±0 pts.

No visual changes against the last default-branch run.

Pages visited
Page Status ms Controls operated Forms submitted Problems
/ 200 9014 0/0 0/0 0

AI explorer not run: no major/framework signal in this PR. Add the ai-explore label to request one.

Screenshots, diffs and the app log: runtime-gate artifact.

@rishitank rishitank left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review by Claude. CodeRabbit is rate-limited on this repo (free OSS review limit), so it gave no line-by-line review.

What I checked (the one changed file, .github/workflows/branch-cleanup.yml, read in full):

  • Injection. No ${{ }} inside run:. The head ref, default branch, repo and token reach the shell only through env:, and every expansion is quoted. The ref is URL-encoded with jq @uri before it goes into the API path.
  • Permissions. The workflow default is {}. The job gets only contents: write (to delete the ref) and pull-requests: read (for gh pr list). No secrets beyond github.token.
  • Scope. The job runs only on merged PRs whose head is in this repository, so a fork's branch can never be targeted.
  • Fail-safe. set -euo pipefail means any API error in the open-PR check fails the job and keeps the branch. Only "Reference does not exist" (422) or a 404 is treated as already deleted. A 403 fails red, so a permission problem is never silent.
  • Allowlist. Covers the default branch, main, master, develop, staging, test, gh-pages, deploy-* and release/*, plus any branch that an open PR uses as its base.
  • Lint. actionlint and shellcheck are clean locally and in CI (the actionlint check is green). The script was also run against a stub gh for every branch above.

Fixed: nothing needed.

Left for Rishi: like GitHub's own auto-delete, this doesn't check whether commits were pushed to the branch after the merge. It deletes the ref regardless, though the commits stay reachable through refs/pull/<n>/head.

@rishitank
rishitank merged commit 36dbd32 into master Oct 9, 2026
8 checks passed
@github-actions
github-actions Bot deleted the ci/branch-cleanup branch October 9, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant