grafana / grafana/shared-workflows
cleanup-branches: skipping every branch with protected=true is too broad — policy-only rulesets (signed commits / required workflows) block all deletions
- Dominant language
- Go
- Stars
- 26
- Forks
- 49
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 36
Description
We're having trouble applying `actions/cleanup-branches` in github.com/grafana/backend-enterprise, as no stale branches are found. Claude Code identified the root problem as being that the action identifies all branches as protected. See the following for rationale:
## Summary
`actions/cleanup-branches` treats any branch returned by `GET /repos/{owner}/{repo}/branches?protected=true` as "must keep" and skips it. But `protected=true` is set by **any** ruleset or branch protection - including policy-only rulesets that place no restriction on deletion (e.g. *require signed commits*, *require workflows to pass*).
On a repo (or org) that applies such a ruleset to **all** branches, every branch reports `protected=true`, so the action skips everything and deletes nothing. The run still succeeds and prints `🟢 No branches marked for deletion.`, so the failure is **silent** - it looks like there's simply nothing to clean up.
## Affected versions
Present since the action was introduced (#1334) and unchanged through `main` and the latest release `cleanup-branches/v0.3.1`. The protected/skip logic has not been touched since #1334.
## Root cause
In `actions/cleanup-branches/action.yml`:
- **~line 42** — the skip-list is built from the coarse flag:
```bash
readarray -t protected_branches < <(gh api --paginate "/repos/${GITHUB_REPOSITORY}/branches?protected=true" | jq -cr '.[].name')
```
- **~lines 65-67** — the main loop drops any branch whose name is in `protected_branches`.
`protected=true` in the REST branches API means the branch is covered by *some* protection/ruleset, regardless of *which rules* it enforces. It is **not** a signal that deletion is disallowed.
## Reproduction
1. Add a branch ruleset (repo or org) targeting **all** branches whose only rule is `required_signatures` (or required `workflows`) — i.e. a policy with **no** `deletion` rule.
2. Have stale branches older than `max-date`, with no open PR, not matching `exclude-patterns`.
3. Run the action.
Expected: the stale branches are listed/deleted. Actual: `🟢 No branches marked for deletion.`, because every branch is now `protected=true`.
## Evidence (real repo, ~2,048 branches)
- A dispatch on **2026-06-10** listed **1,121** branches to delete (working as intended).
- After org rulesets were broadened to cover all branches, a scheduled run on **2026-07-06** and a dry-run on **2026-07-08** both listed **0**.
- Instrumenting the action's own selection logic in CI:
```
branches=2047 open_pr=133 protected=2048 <-- every branch reports protected
OLDER-THAN-MAXDATE(no-exclusions)=1839
```
- The two rulesets that target all branches are policy-only (no `deletion` rule):
- **Signed Commits** → `required_signatures`
- **Trufflehog – All Repos** → `workflows`
So all ~1,839 stale branches are genuinely deletable, but the action skips them.
## Suggested fix
Skip a branch only when a rule that actually blocks deletion applies, instead of on the coarse
`protected=true` flag:
- For each candidate branch, query `GET /repos/{owner}/{repo}/rules/branches/{branch}` and skip only if a rule of `type == "deletion"` is present (optionally also `non_fast_forward` if you want to be conservative). This is the most accurate.
- And/or honor classic branch protection where "allow deletions" is disabled.
The deletion-rule check is per-branch, but can be limited to the already-narrowed candidate set (past `max-date`, no open PR, not excluded) to keep API calls bounded.
## Impact
On any repo/org enforcing an all-branches policy ruleset — increasingly common for security hardening (signed commits, secret scanning) — this action silently becomes a no-op. The green run plus the "No branches marked for deletion" message masks it as "nothing to clean up."
Contributor guide
Research direction
Start with actions/cleanup-branches/action.yml, especially the protected_branches query around line 42 and the branch-selection loop around lines 65-67. Check the GitHub rules API behavior for candidate branches and verify that policy-only rulesets no longer suppress stale branches, while rules that block deletion still do. Done means the action lists or deletes eligible stale branches instead of silently reporting none.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github, github-actions, shell
- Domain
- ci-cd, devops
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100