|
| 1 | +name: Clean up orphaned git branches |
| 2 | +description: | |
| 3 | + This action will query for branches that are not in an open PR, and will delete them if 'dry-run' is 'false'. |
| 4 | + Protected branches are excluded as well. |
| 5 | +inputs: |
| 6 | + dry-run: |
| 7 | + default: "true" |
| 8 | + required: true |
| 9 | + description: "If 'true', then the action will print branches to be deleted, but will not delete them" |
| 10 | + token: |
| 11 | + default: ${{ github.token }} |
| 12 | + required: true |
| 13 | + description: "GitHub token used to authenticate with `gh`. Requires permission to query for protected branches and delete branches (contents: write) and pull requests (pull_requests: read)" |
| 14 | + max-date: |
| 15 | + default: "2 weeks ago" |
| 16 | + required: false |
| 17 | + description: | |
| 18 | + Value provided to `date -d={}. From `man date`: "The --date=STRING is a mostly free format human readable date string such as "Sun, 29 Feb 2004 16:21:42 -0800" or "2004-02-29 16:21:42" or even "next Thursday". A date string may |
| 19 | + contain items indicating calendar date, time of day, time zone, day of week, relative time, relative date, and numbers. An empty string indicates the beginning of the day. The |
| 20 | + date string format is more complex than is easily documented here but is fully described in the info documentation." |
| 21 | +runs: |
| 22 | + using: composite |
| 23 | + steps: |
| 24 | + - name: List branches |
| 25 | + shell: bash |
| 26 | + env: |
| 27 | + GH_TOKEN: ${{ inputs.token }} |
| 28 | + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} |
| 29 | + MAX_DATE: ${{ inputs.max-date }} |
| 30 | + run: | |
| 31 | + #!/usr/bin/env bash |
| 32 | + # Fetch all branches from remote. This is basically `git fetch --unshallow` but also fetches branches. |
| 33 | + git fetch origin "+refs/heads/*:refs/remotes/origin/*" |
| 34 | +
|
| 35 | + # A limit of 1,000 open PRs is far beyond any repository that is in the grafana org |
| 36 | + readarray -t open_pr_branches < <(gh pr list --state open -L 1000 --json headRefName | jq -cr '.[].headRefName') |
| 37 | +
|
| 38 | + # For repositories that have exceeded 2,000+ branches, this could fail. |
| 39 | + readarray -t protected_branches < <(gh api --paginate "/repos/${GITHUB_REPOSITORY}/branches?protected=true" | jq -cr '.[].name') |
| 40 | +
|
| 41 | + branches=() |
| 42 | + while IFS= read -r line; do |
| 43 | + branches+=("$line") |
| 44 | + done < <(git ls-remote --heads origin | awk '{print $2}' | sed 's|refs/heads/||' | grep -Ev "^(origin/)?(${DEFAULT_BRANCH})$") |
| 45 | +
|
| 46 | + to_delete=() |
| 47 | + for branch in "${branches[@]}"; do |
| 48 | + found=0 |
| 49 | + for pr_branch in "${open_pr_branches[@]}"; do |
| 50 | + if [[ "$branch" == "$pr_branch" ]]; then |
| 51 | + found=1 |
| 52 | + break |
| 53 | + fi |
| 54 | + done |
| 55 | + if [ "$found" != 1 ]; then |
| 56 | + for protected_branch in "${protected_branches[@]}"; do |
| 57 | + if [[ "$branch" == "$protected_branch" ]]; then |
| 58 | + found=1 |
| 59 | + break |
| 60 | + fi |
| 61 | + done |
| 62 | + fi |
| 63 | + if [ "$found" != 1 ]; then |
| 64 | + to_delete+=("$branch") |
| 65 | + fi |
| 66 | + done |
| 67 | + max_date=$(TZ=utc date -d "$MAX_DATE" +%s) |
| 68 | + for branch in "${to_delete[@]}"; do |
| 69 | + branch_ts=$(git log -1 --format=%ct "origin/$branch") |
| 70 | + if [[ "$branch_ts" -lt "$max_date" ]]; then |
| 71 | + echo "$branch" >> branches.txt |
| 72 | + fi |
| 73 | + done |
| 74 | + - name: Delete branches (dry run) |
| 75 | + shell: bash |
| 76 | + if: ${{ inputs.dry-run == "true" }} |
| 77 | + run: | |
| 78 | + cat branches.txt | xargs -I {} echo git push origin --delete "{}" |
| 79 | + - name: Delete branches |
| 80 | + shell: bash |
| 81 | + if: ${{ inputs.dry-run != "true" }} |
| 82 | + run: | |
| 83 | + cat branches.txt | xargs -I {} git push origin --delete "{}" |
0 commit comments