Schedule icon
Script icon
Docker icon
If icon
SlackIncomingWebhook icon

BranchJanitor - GitHub stale branch cleaner

A Kestra blueprint that finds stale GitHub branches, deletes them safely, and posts a Slack summary, with a safe dry-run default.

Categories
Infrastructure

How it works

  1. find_stale (io.kestra.plugin.scripts.python.Script) lists every branch through the GitHub API and decides what is stale. A branch is stale when its pull request was merged, or when it has no PR at all and its last commit is older than stale_days. The default branch, protected branches, and branches with open PRs are never touched. For forks, pass upstream_repo: PRs are looked up on the upstream repo while branches are cleaned on your fork.
  2. maybe_clean (io.kestra.plugin.core.flow.If) runs only when something is stale. Inside it, do_clean runs only in live mode (dry_run: false) and deletes each stale branch with the GitHub API. send_summary always posts the report to Slack: what would be deleted in dry-run mode, what was deleted in live mode.
  3. There is no KV state in this flow, and that is deliberate. A janitor does not need memory: once a branch is deleted it is gone, and dry-run mode is a report, not an alert.

What you get

  • Automatic cleanup of stale branches on a schedule, no more 200-branch repos.
  • A Slack report every run that found something: which branches, why each one counted as stale, and whether it was a dry run or a real cleanup.
  • Safety by default: dry_run is true unless you explicitly turn it off, and merged-or-abandoned is the only thing that ever gets deleted.
  • Quiet runs when there is nothing stale to clean.

Who it's for

Anyone whose repos have accumulated dozens of branches from merged PRs nobody deleted. Works on any GitHub repo you have push access to, personal or org.

Why orchestrate this with Kestra

A shell script on a cron server can call the GitHub API, but then you need somewhere to hold the token, a place to see the run history when a branch got deleted that should not have been, and a safe dry-run mode you can trust. Kestra gives you all three: the token stays in secrets out of the YAML, every run keeps its logs and outputs where you can audit them, and the dry-run default means the first run can never surprise you.

Prerequisites

  • A GitHub personal access token with contents: write on the target repo (classic repo scope works too).
  • Kestra with the Docker task runner available (used for the Python steps).
  • A Slack Incoming Webhook URL.

Secrets

Secret What it is
GITHUB_TOKEN base64-encoded GitHub personal access token with push access to the repo
SLACK_WEBHOOK base64-encoded Slack Incoming Webhook URL

In Kestra OSS, secrets come from environment variables prefixed with SECRET_ and must be base64-encoded.

Quick start

  1. Start Kestra with the two secrets above as SECRET_ environment variables.
  2. Import this flow and run it with your repo as owner/repo and dry_run: true: the Slack report lists what would be deleted, and nothing is touched. If you work from a fork, also pass upstream_repo as the upstream owner/repo so merged PRs are found.
  3. Check the list. If it looks right, run once with dry_run: false: the branches are deleted and the Slack report says so.
  4. Run again: quiet, nothing stale left.

How to extend

  • Exclude branches by pattern (like release/*) with a name filter in the Python step.
  • Auto-delete branches when their PR merges by triggering on the pull_request closed webhook instead of the schedule.
  • Lower the schedule to daily for busy repos by changing the cron trigger.
  • Route reports to PagerDuty or Opsgenie by swapping the Slack task for another notification task.

Inputs

Input Type Default What it does
repo STRING (required) the repo whose branches to clean, as owner/repo
upstream_repo STRING "" for forks only, the upstream repo as owner/repo. PRs are looked up there, branches are cleaned on repo
stale_days INT 90 branches with no PR and no commit for this long count as abandoned
dry_run BOOL true when true, only reports what would be deleted without deleting anything
slack_webhook STRING (required) Incoming webhook URL of the Slack channel for the cleanup report

Links

Pitfalls

  • dry_run defaults to true for a reason. Flip it to false only after you have seen a dry-run report and you trust the list.
  • The token needs push access to the repo, otherwise the delete calls return 403 and the run fails loudly. That is intentional: a silent skip would be worse.
  • The stale check is branch-based, not PR-based. A branch with an open PR is never touched, even if its last commit is ancient.
  • The default branch and protected branches are skipped no matter what. There is no input that overrides this.
See How

New to Kestra?

Use blueprints to kickstart your first workflows.