From 760ab1dd30157a67f930afd8511a1cb8f97b8065 Mon Sep 17 00:00:00 2001 From: Philipp Wallrafen <74021231+philippwallrafen@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:39:37 +0200 Subject: [PATCH 1/3] chore: add behind branch cleanup script --- scripts/delete-pheaz-behind-branches.sh | 262 ++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 scripts/delete-pheaz-behind-branches.sh diff --git a/scripts/delete-pheaz-behind-branches.sh b/scripts/delete-pheaz-behind-branches.sh new file mode 100644 index 0000000..11e2dd0 --- /dev/null +++ b/scripts/delete-pheaz-behind-branches.sh @@ -0,0 +1,262 @@ +#!/usr/bin/env bash +# /scripts/delete-pheaz-behind-branches.sh + +ORG="pheaz" + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + printf 'Missing required command: %s\n' "$1" >&2 + return 2 + fi +} + +delete_branch() { + local repo="$1" + local default_branch="$2" + local expected_default_sha="$3" + local branch="$4" + local expected_branch_sha="$5" + + local current_default_branch + local current_default_sha + local current_branch_sha + local delete_output + + if ! current_default_branch="$( + gh api --method GET \ + "repos/${repo}" \ + --jq '.default_branch' 2>/dev/null + )"; then + printf ' [FAIL] %-32s could not re-read repository settings\n' \ + "$branch" >&2 + return 1 + fi + + if [ "$current_default_branch" != "$default_branch" ]; then + printf ' [CHANGED] %-30s default branch changed; preserving\n' \ + "$branch" + return 0 + fi + + if ! current_default_sha="$( + gh api --method GET \ + "repos/${repo}/git/ref/heads/${default_branch}" \ + --jq '.object.sha' 2>/dev/null + )"; then + printf ' [FAIL] %-32s could not re-read default branch ref\n' \ + "$branch" >&2 + return 1 + fi + + if [ "$current_default_sha" != "$expected_default_sha" ]; then + printf ' [CHANGED] %-30s default branch moved; preserving\n' \ + "$branch" + return 0 + fi + + if ! current_branch_sha="$( + gh api --method GET \ + "repos/${repo}/git/ref/heads/${branch}" \ + --jq '.object.sha' 2>/dev/null + )"; then + printf ' [CHANGED] %-30s branch no longer readable; preserving\n' \ + "$branch" + return 0 + fi + + if [ "$current_branch_sha" != "$expected_branch_sha" ]; then + printf ' [CHANGED] %-30s branch moved during scan; preserving\n' \ + "$branch" + return 0 + fi + + if delete_output="$( + gh api --method DELETE \ + "repos/${repo}/git/refs/heads/${branch}" \ + --silent 2>&1 + )"; then + printf ' [DELETE] %-31s %s\n' "$branch" "$expected_branch_sha" + return 0 + fi + + printf ' [FAIL] %-32s\n%s\n' "$branch" "$delete_output" >&2 + return 1 +} + +scan_repository() { + local repo="$1" + local default_branch="$2" + + local default_sha + local branches + + if ! default_sha="$( + gh api --method GET \ + "repos/${repo}/git/ref/heads/${default_branch}" \ + --jq '.object.sha' 2>/dev/null + )"; then + printf '[FAIL] %-40s could not resolve default branch %s\n' \ + "$repo" "$default_branch" >&2 + return 1 + fi + + if ! branches="$( + gh api --paginate --method GET \ + "repos/${repo}/branches" \ + -f per_page=100 \ + --jq '.[] | [.name, .commit.sha] | @tsv' + )"; then + printf '[FAIL] %-40s could not list branches\n' "$repo" >&2 + return 1 + fi + + printf '\n[REPO] %s\n' "$repo" + printf ' Default: %s (%s)\n' "$default_branch" "$default_sha" + + local branch + local branch_sha + local comparison + local ahead + local behind + + local scanned=0 + local preserved=0 + local deleted=0 + local changed=0 + local failed=0 + + while IFS=$'\t' read -r branch branch_sha; do + [ -n "$branch" ] || continue + + if [ "$branch" = "$default_branch" ]; then + continue + fi + + scanned=$((scanned + 1)) + + if ! comparison="$( + gh api --method GET \ + "repos/${repo}/compare/${default_sha}...${branch_sha}" \ + --jq '[.ahead_by, .behind_by] | @tsv' 2>/dev/null + )"; then + failed=$((failed + 1)) + printf ' [FAIL] %-32s comparison failed\n' "$branch" >&2 + continue + fi + + IFS=$'\t' read -r ahead behind <<< "$comparison" + + if [ "$ahead" -ne 0 ]; then + preserved=$((preserved + 1)) + printf ' [KEEP] %-32s ahead=%s behind=%s\n' \ + "$branch" "$ahead" "$behind" + continue + fi + + if [ "$behind" -eq 0 ]; then + preserved=$((preserved + 1)) + printf ' [KEEP] %-32s identical to default\n' "$branch" + continue + fi + + if delete_branch \ + "$repo" \ + "$default_branch" \ + "$default_sha" \ + "$branch" \ + "$branch_sha"; then + deleted=$((deleted + 1)) + else + failed=$((failed + 1)) + fi + done <<< "$branches" + + printf ' Summary\n' + printf ' Branches scanned: %d\n' "$scanned" + printf ' Preserved: %d\n' "$preserved" + printf ' Deleted: %d\n' "$deleted" + printf ' Failed: %d\n' "$failed" + + if [ "$failed" -gt 0 ]; then + return 1 + fi + + return 0 +} + +main() { + require_command gh || return 2 + + if ! gh auth status >/dev/null 2>&1; then + printf 'GitHub CLI is not authenticated. Run: gh auth login\n' >&2 + return 2 + fi + + local repositories + + if ! repositories="$( + gh api --paginate --method GET \ + "orgs/${ORG}/repos" \ + -f per_page=100 \ + -f type=all \ + --jq ' + .[] + | select(.fork == false) + | [.full_name, (.archived | tostring), .default_branch] + | @tsv + ' + )"; then + printf 'Could not list repositories for organization: %s\n' \ + "$ORG" >&2 + return 2 + fi + + local scanned=0 + local skipped=0 + local completed=0 + local failed=0 + + local repo + local archived + local default_branch + + printf 'Organization: %s\n' "$ORG" + printf 'Policy: delete branches with ahead=0 and behind>0\n' + + while IFS=$'\t' read -r repo archived default_branch; do + [ -n "$repo" ] || continue + scanned=$((scanned + 1)) + + if [ "$archived" = "true" ]; then + skipped=$((skipped + 1)) + printf '\n[SKIP] %-40s archived repository\n' "$repo" + continue + fi + + if [ -z "$default_branch" ] || [ "$default_branch" = "null" ]; then + skipped=$((skipped + 1)) + printf '\n[SKIP] %-40s no default branch\n' "$repo" + continue + fi + + if scan_repository "$repo" "$default_branch"; then + completed=$((completed + 1)) + else + failed=$((failed + 1)) + fi + done <<< "$repositories" + + printf '\nOrganization summary\n' + printf ' Non-fork repos scanned: %d\n' "$scanned" + printf ' Repos skipped: %d\n' "$skipped" + printf ' Repos completed: %d\n' "$completed" + printf ' Repos with failures: %d\n' "$failed" + + if [ "$failed" -gt 0 ]; then + return 3 + fi + + return 0 +} + +main "$@" From b67e0320ce35c0c7af745f01a9c4080b485c9d5e Mon Sep 17 00:00:00 2001 From: Philipp Wallrafen <74021231+philippwallrafen@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:40:57 +0200 Subject: [PATCH 2/3] fix: distinguish preserved branches during cleanup --- scripts/delete-pheaz-behind-branches.sh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/delete-pheaz-behind-branches.sh b/scripts/delete-pheaz-behind-branches.sh index 11e2dd0..c9f3b67 100644 --- a/scripts/delete-pheaz-behind-branches.sh +++ b/scripts/delete-pheaz-behind-branches.sh @@ -35,7 +35,7 @@ delete_branch() { if [ "$current_default_branch" != "$default_branch" ]; then printf ' [CHANGED] %-30s default branch changed; preserving\n' \ "$branch" - return 0 + return 10 fi if ! current_default_sha="$( @@ -51,7 +51,7 @@ delete_branch() { if [ "$current_default_sha" != "$expected_default_sha" ]; then printf ' [CHANGED] %-30s default branch moved; preserving\n' \ "$branch" - return 0 + return 10 fi if ! current_branch_sha="$( @@ -61,13 +61,13 @@ delete_branch() { )"; then printf ' [CHANGED] %-30s branch no longer readable; preserving\n' \ "$branch" - return 0 + return 10 fi if [ "$current_branch_sha" != "$expected_branch_sha" ]; then printf ' [CHANGED] %-30s branch moved during scan; preserving\n' \ "$branch" - return 0 + return 10 fi if delete_output="$( @@ -118,11 +118,12 @@ scan_repository() { local comparison local ahead local behind + local delete_status local scanned=0 local preserved=0 - local deleted=0 local changed=0 + local deleted=0 local failed=0 while IFS=$'\t' read -r branch branch_sha; do @@ -159,13 +160,18 @@ scan_repository() { continue fi - if delete_branch \ + delete_branch \ "$repo" \ "$default_branch" \ "$default_sha" \ "$branch" \ - "$branch_sha"; then + "$branch_sha" + delete_status=$? + + if [ "$delete_status" -eq 0 ]; then deleted=$((deleted + 1)) + elif [ "$delete_status" -eq 10 ]; then + changed=$((changed + 1)) else failed=$((failed + 1)) fi @@ -174,6 +180,7 @@ scan_repository() { printf ' Summary\n' printf ' Branches scanned: %d\n' "$scanned" printf ' Preserved: %d\n' "$preserved" + printf ' Changed mid-scan: %d\n' "$changed" printf ' Deleted: %d\n' "$deleted" printf ' Failed: %d\n' "$failed" From 572c58bb3339fa5a7b8507a615c62c3a38b88232 Mon Sep 17 00:00:00 2001 From: Philipp Wallrafen <74021231+philippwallrafen@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:41:18 +0200 Subject: [PATCH 3/3] chore: make behind branch cleanup script executable --- scripts/delete-pheaz-behind-branches.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/delete-pheaz-behind-branches.sh diff --git a/scripts/delete-pheaz-behind-branches.sh b/scripts/delete-pheaz-behind-branches.sh old mode 100644 new mode 100755