Skip to content

feat(ci): dead-branch sweeper (dry-run by default, merged-only delete) - #6

Merged
GeGGe01 merged 2 commits into
mainfrom
feat/dead-branch-sweep
Jul 13, 2026
Merged

feat(ci): dead-branch sweeper (dry-run by default, merged-only delete)#6
GeGGe01 merged 2 commits into
mainfrom
feat/dead-branch-sweep

Conversation

@ghost

@ghost ghost commented Jul 13, 2026

Copy link
Copy Markdown

Summary

Central, org-wide dead-branch sweeper (dead-branch-sweep.yml +
scripts/dead-branch-sweep.sh + docs/branch-sweep.md), operator directive:
"vi ska ha en sweeper på döda branches."

  • Deletes ONLY fully-merged branches. Merged is decided two ways OR-ed
    together: commit-containment (compare base...branch = identical/behind)
    and merged-PR history (closed PR with merged_at set) — the second
    check exists specifically because squash/rebase merges break plain
    commit-containment detection.
  • Never deletes: the repo's default branch, the mnab set
    (main/next/before/after), anything GitHub itself reports as
    protected, or any branch with an open PR — regardless of merge state.
  • Stale unmerged branches are report-only, never deleted: no open PR +
    not merged + idle > stale_days (default 90) → listed in the job summary
    and a JSON artifact for triage. An unmerged branch with no PR is the
    "stalled but maybe-real-work" signal, not garbage — per the legibility
    doctrine.
  • Ships inert. Scheduled (weekly, Sun 03:17 UTC) runs stay dry-run unless
    the operator sets a BRANCH_SWEEP_ARMED=true repo/org variable — a
    separate, deliberate step outside this PR. Manual workflow_dispatch also
    defaults dry_run: true.
  • Least-privilege token (BRANCH_SWEEP_TOKEN secret): Contents: write
    • Pull requests: read only. Scope note in docs/branch-sweep.md: this
      only reaches repos the fleet-owned token can see — alfred-intelligence
      only, not GeGGe01/SAVANTERNA/kebab-it.
  • Target-state auth note: this exact permission profile matches the
    already-decided-but-not-yet-registered aifred-maintenance App
    (.github-private/strategy/meta-apps.md). Documented as the migration
    path once that App exists; not blocking this PR.

Governance conformance

Checked .github-private/DECISIONS.md (current through bca3b2a) before
building: no existing entry authorizes org-wide auto-delete of merged
branches. This PR does not add that DECISIONS.md entry — it ships the
mechanism dry-run/inert and flags in docs/branch-sweep.md ("Governance
note") that a short DECISIONS.md post should land before anyone ever sets
BRANCH_SWEEP_ARMED=true. Does not fight the mnab CI-gate-gradient or
branch-protection config — never touches protected branches or required
checks.

Setup needed before first real (non-dry-run) use

  1. Create BRANCH_SWEEP_TOKEN (fine-grained PAT, bot-owned, Contents: R+W
    • Pull requests: R, all-repos-in-org) — see docs/branch-sweep.md.
  2. Run a manual workflow_dispatch with dry_run: true (default) and
    eyeball the job summary.
  3. Only then consider setting BRANCH_SWEEP_ARMED=true — and only after the
    governance note above is addressed.

Test plan

  • python3 -c "import yaml; yaml.safe_load(...)" — workflow YAML valid
  • shellcheck scripts/dead-branch-sweep.sh — clean
  • Operator: set BRANCH_SWEEP_TOKEN, run workflow_dispatch dry-run
    against a small repos_override list, verify job summary output
    before trusting an org-wide dry-run
  • Operator: review docs/branch-sweep.md governance note, decide on a
    DECISIONS.md entry before arming

🤖 Generated with Claude Code

https://claude.ai/code/session_01JGpSmSMHtG3rP13yq5sU1g

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@ghost ghost assigned GeGGe01 Jul 13, 2026
@pr-insights-tagger

Copy link
Copy Markdown

PR Analysis Summary

Risk Level Complexity Files Changed

📧 Email-friendly summary
Risk: 🔴 High Risk | Complexity: 🟢 4.56/10 | Files: 6

Change Metrics

Metric Value
Lines Added +765
Lines Deleted -1
Files Modified 6
Complexity Score 🟢 4.56/10
Risk Assessment 🔴 High Risk

Risk factors:

  • .github/workflows/dead-branch-sweep.yml (CI/CD workflow changes)
  • .github/workflows/mnab-gate.yml (CI/CD workflow changes)

Classification

size:large type:feature risk:high complexity:medium lang:shell docs:markdown config:files

Files by Type

md ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 3 files
yml ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 2 files
sh ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 1 file

💡 Recommendations

  1. High-risk changes detected - extra review attention recommended
  2. Significant code additions - ensure adequate test coverage

Analyzed by Woden Tagger • Automated PR insights for better code reviews
💡 Want enhanced Github Projects/Notes and to turn Issues into a Service Desk? Check out Woden Desk on the GitHub Marketplace

Comment thread .github/workflows/mnab-gate.yml Fixed
Mr-RedHat-fb and others added 2 commits July 13, 2026 13:05
Central, org-wide job (register-skill.yml's shape, not a per-repo
workflow_call) that deletes only branches fully merged into their repo's
default branch — checked both by commit-containment and by merged-PR
history, so squash/rebase merges are still recognised as merged.

Never touches the default branch, the mnab set (main/next/before/after),
anything GitHub reports as protected, or any branch with an open PR.
Unmerged branches idle > stale_days (default 90) with no open PR are
report-only in the job summary + JSON artifact — the "stalled but
maybe-real work" signal the legibility doctrine says to surface, not
erase.

Ships inert: scheduled runs stay dry-run unless the operator sets
BRANCH_SWEEP_ARMED=true; manual dispatch also defaults dry_run=true.

Token model documented in docs/branch-sweep.md: interim bot-owned
fine-grained PAT (Contents: write, Pull requests: read, least privilege),
target-state migration path to the already-decided-but-unregistered
aifred-maintenance App noted for when that App exists.

Governance note: no DECISIONS.md entry currently authorizes org-wide
auto-delete of merged branches. This PR does not add one — it ships
inert (dry-run) and flags that a short DECISIONS.md post should land
before BRANCH_SWEEP_ARMED is ever set to true anywhere. See
docs/branch-sweep.md "Governance note" section.

Shellcheck-clean (scripts/dead-branch-sweep.sh), YAML-validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGpSmSMHtG3rP13yq5sU1g
Security review (inquisitor) + evidence pass (referent) found blockers on
PR#6 before merge, fixed here:

- F1 [CRITICAL] data-loss: has_merged_pr only counted a closed PR's
  merged_at, matched by branch NAME. GitHub keeps closed-PR records
  forever, so a reused branch name (force-pushed with new unmerged commits
  after an earlier PR on that name merged, or deleted+recreated) would
  still read as merged via the stale record even though compare correctly
  reported diverged -> genuinely unmerged work could get deleted. Now
  requires the merged PR's head.sha to equal the branch's current tip
  before trusting it; a genuine squash/rebase merge of the current tip
  still matches, since the tip hasn't moved since GitHub recorded it.

- F2 [HIGH] query-param injection: branch names can legally contain
  '&'/'='/'#' (git ref rules don't forbid them) and were spliced raw into
  `pulls?head=...&state=all`, so a crafted branch name could inject a
  second head= param and read a different branch's PR history. Switched to
  `gh api -f head=... -f state=all` (gh encodes the query for us) and
  percent-encode every branch name used in a REST path segment
  (branches/<name>, compare/<base>...<name>, git/refs/heads/<name>).

- F5 [MED] SHA-pin actions/checkout + actions/upload-artifact in
  dead-branch-sweep.yml. Repo convention (72f78e9) deliberately leaves
  first-party actions/* on floating tags; this workflow is an explicit
  exception because the Sweep step shares a job with BRANCH_SWEEP_TOKEN
  (org-wide contents:write) -- noted inline in the workflow why this one
  diverges from the general convention.

- Reconciled against current main (referent's regression finding): PR#6's
  branch predated main's CodeQL "Code injection" fix to mnab-gate.yml
  (build-command allowlist). Rebased onto origin/main -- git recognised
  the branch's own mnab-gate.yml/pin-SHA/light-gates-docs commits as
  already-applied-equivalent patches and dropped them automatically, so
  the only real conflict was a README.md table-row merge. Diffed clean
  afterward: dead-branch-sweep's diff against main is now only the three
  new sweep files, no stale mnab-gate.yml copy.

docs/branch-sweep.md: added a second-gate note -- BRANCH_SWEEP_ARMED must
also confirm the head.sha-match fix is still present in the script, not
just that a DECISIONS.md authorization entry exists (F1 fix must stay
landed, per inquisitor O1).

Does not merge -- operator's review gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGpSmSMHtG3rP13yq5sU1g
@GeGGe01
GeGGe01 force-pushed the feat/dead-branch-sweep branch from 003a4eb to ae4c884 Compare July 13, 2026 11:10
@pr-insights-tagger

Copy link
Copy Markdown

PR Analysis Summary

Risk Level Complexity Files Changed

📧 Email-friendly summary
Risk: 🔴 High Risk | Complexity: 🟢 3.75/10 | Files: 4

Change Metrics

Metric Value
Lines Added +485
Lines Deleted -0
Files Modified 4
Complexity Score 🟢 3.75/10
Risk Assessment 🔴 High Risk

Risk factors:

  • .github/workflows/dead-branch-sweep.yml (CI/CD workflow changes)

Classification

size:medium type:feature risk:high lang:shell docs:markdown config:files

Files by Type

md ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 2 files
yml ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 1 file
sh ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 1 file

💡 Recommendations

  1. High-risk changes detected - extra review attention recommended
  2. Significant code additions - ensure adequate test coverage

Analyzed by Woden Tagger • Automated PR insights for better code reviews
💡 Want enhanced Github Projects/Notes and to turn Issues into a Service Desk? Check out Woden Desk on the GitHub Marketplace

@ghost

ghost commented Jul 13, 2026

Copy link
Copy Markdown
Author

Security review fixes pushed (F1/F2/F5 from inquisitor's review + referent's evidence pass):

  • F1 [CRITICAL] data-loss: merged-PR detection now requires the merged PR's head.sha to match the branch's current tip, closing the reused-branch-name stale-record path that could delete genuinely unmerged work.
  • F2 [HIGH] query-injection: PR-history lookup now uses gh api -f/-f (auto-encoded) instead of hand-spliced query string; every branch name in a REST path is percent-encoded.
  • F5 [MED] actions/checkout and actions/upload-artifact SHA-pinned in dead-branch-sweep.yml (explicit exception to this repo's checkout-stays-on-tag convention, noted inline — justified by BRANCH_SWEEP_TOKEN sharing the job).
  • Rebased onto current main to pick up the CodeQL mnab-gate.yml fix that had landed after this branch diverged — diff against main is now only the three new sweep files.

docs/branch-sweep.md governance note updated: the future DECISIONS.md arming authorization must also confirm the F1 head-SHA-match fix is still present in the script, not just that a governance entry exists.

Not merging — this stays on the operator's review gate.

@sonarqubecloud

Copy link
Copy Markdown

@GeGGe01
GeGGe01 merged commit 4929772 into main Jul 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants