From f8a80e27ef576520219bece7a03b1fd8d1b6bfd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:39:51 +0000 Subject: [PATCH 1/6] Initial plan From d663446acef18ec5188205ec58a314ac31029a9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:43:52 +0000 Subject: [PATCH 2/6] Add cleanup workflow for lint branches Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .github/workflows/cleanup-lint-branches.yml | 175 ++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 .github/workflows/cleanup-lint-branches.yml diff --git a/.github/workflows/cleanup-lint-branches.yml b/.github/workflows/cleanup-lint-branches.yml new file mode 100644 index 00000000..5026557d --- /dev/null +++ b/.github/workflows/cleanup-lint-branches.yml @@ -0,0 +1,175 @@ +name: "๐Ÿงน Cleanup Lint Branches" + +# This workflow automatically cleans up lint/* branches that have been merged or closed. +# It helps maintain a clean repository by removing branches created by the linter workflow +# after their associated pull requests are no longer active. + +on: + # Run daily at 00:00 UTC + schedule: + - cron: '0 0 * * *' + + # Allow manual triggering with optional dry-run mode + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run mode (preview deletions without actually deleting)' + required: false + type: boolean + default: true + +permissions: + contents: write + pull-requests: read + +jobs: + cleanup: + name: Clean up merged/closed lint branches + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all branches and history + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup GitHub CLI + run: | + # Verify gh CLI is available (pre-installed on ubuntu-latest) + gh --version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Cleanup lint branches + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} + BRANCH_AGE_DAYS: 7 + run: | + set -e + + echo "๐Ÿงน Starting lint branch cleanup..." + echo "Dry run mode: $DRY_RUN" + echo "Branch age threshold for orphaned branches: $BRANCH_AGE_DAYS days" + echo "" + + # Get current date in seconds since epoch + current_date=$(date +%s) + age_threshold_seconds=$((BRANCH_AGE_DAYS * 86400)) + + # Track statistics + total_branches=0 + deleted_branches=0 + skipped_branches=0 + error_branches=0 + + # Get all remote branches matching the pattern lint/style-fixes-* + echo "๐Ÿ“‹ Fetching lint branches..." + lint_branches=$(git ls-remote --heads origin 'refs/heads/lint/style-fixes-*' | awk '{print $2}' | sed 's#refs/heads/##' || echo "") + + if [ -z "$lint_branches" ]; then + echo "โœ… No lint branches found matching 'lint/style-fixes-*'" + exit 0 + fi + + echo "Found $(echo "$lint_branches" | wc -l) lint branches" + echo "" + + # Process each branch + for branch in $lint_branches; do + total_branches=$((total_branches + 1)) + echo "๐Ÿ” Processing: $branch" + + # Extract run ID from branch name (e.g., lint/style-fixes-17199517932) + run_id=$(echo "$branch" | sed 's/lint\/style-fixes-//') + + # Check if there's a PR for this branch + pr_number=$(gh pr list --state all --head "$branch" --json number --jq '.[0].number' 2>/dev/null || echo "") + + should_delete=false + delete_reason="" + + if [ -n "$pr_number" ]; then + # PR exists, check its state + echo " Found PR #$pr_number" + + pr_state=$(gh pr view "$pr_number" --json state --jq '.state' 2>/dev/null || echo "") + pr_merged=$(gh pr view "$pr_number" --json merged --jq '.merged' 2>/dev/null || echo "false") + + if [ "$pr_merged" = "true" ]; then + should_delete=true + delete_reason="PR #$pr_number was merged" + elif [ "$pr_state" = "CLOSED" ]; then + should_delete=true + delete_reason="PR #$pr_number was closed" + else + echo " โญ๏ธ Skipping: PR #$pr_number is still open" + skipped_branches=$((skipped_branches + 1)) + fi + else + # No PR found - check if branch is old enough to delete + echo " No associated PR found (orphaned branch)" + + # Get the last commit date on this branch + last_commit_date=$(git log -1 --format=%ct "origin/$branch" 2>/dev/null || echo "0") + + if [ "$last_commit_date" != "0" ]; then + branch_age_seconds=$((current_date - last_commit_date)) + branch_age_days=$((branch_age_seconds / 86400)) + + echo " Branch age: $branch_age_days days" + + if [ $branch_age_seconds -gt $age_threshold_seconds ]; then + should_delete=true + delete_reason="Orphaned branch older than $BRANCH_AGE_DAYS days (age: $branch_age_days days)" + else + echo " โญ๏ธ Skipping: Orphaned branch is only $branch_age_days days old (threshold: $BRANCH_AGE_DAYS days)" + skipped_branches=$((skipped_branches + 1)) + fi + else + echo " โš ๏ธ Warning: Could not determine branch age" + skipped_branches=$((skipped_branches + 1)) + fi + fi + + # Delete the branch if appropriate + if [ "$should_delete" = "true" ]; then + if [ "$DRY_RUN" = "true" ]; then + echo " ๐Ÿ” [DRY RUN] Would delete: $delete_reason" + deleted_branches=$((deleted_branches + 1)) + else + echo " ๐Ÿ—‘๏ธ Deleting: $delete_reason" + if gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/$branch" 2>/dev/null; then + echo " โœ… Successfully deleted" + deleted_branches=$((deleted_branches + 1)) + else + echo " โŒ Failed to delete branch" + error_branches=$((error_branches + 1)) + fi + fi + fi + + echo "" + done + + # Print summary + echo "๐Ÿ“Š Cleanup Summary" + echo "==================" + echo "Total lint branches processed: $total_branches" + if [ "$DRY_RUN" = "true" ]; then + echo "Branches that would be deleted: $deleted_branches" + else + echo "Branches deleted: $deleted_branches" + fi + echo "Branches skipped: $skipped_branches" + if [ $error_branches -gt 0 ]; then + echo "Branches with errors: $error_branches" + fi + echo "" + + if [ "$DRY_RUN" = "true" ]; then + echo "โœ… Dry run completed - no branches were actually deleted" + else + echo "โœ… Cleanup completed" + fi From dd57b761c19d3c2b95eca8dd999265148d79f28e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:45:44 +0000 Subject: [PATCH 3/6] Fix code review issues: safer dry-run defaults, branch validation, and loop handling Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .github/workflows/cleanup-lint-branches.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cleanup-lint-branches.yml b/.github/workflows/cleanup-lint-branches.yml index 5026557d..c9a8582e 100644 --- a/.github/workflows/cleanup-lint-branches.yml +++ b/.github/workflows/cleanup-lint-branches.yml @@ -44,7 +44,9 @@ jobs: - name: Cleanup lint branches env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} + # Default to false for manual triggers with dry_run=false, otherwise true + # This ensures scheduled runs default to true (safe mode) + DRY_RUN: ${{ github.event.inputs.dry_run == 'false' && 'false' || 'true' }} BRANCH_AGE_DAYS: 7 run: | set -e @@ -76,13 +78,19 @@ jobs: echo "Found $(echo "$lint_branches" | wc -l) lint branches" echo "" - # Process each branch - for branch in $lint_branches; do + # Process each branch (using while read to handle special characters safely) + while IFS= read -r branch; do + [ -z "$branch" ] && continue total_branches=$((total_branches + 1)) echo "๐Ÿ” Processing: $branch" - # Extract run ID from branch name (e.g., lint/style-fixes-17199517932) - run_id=$(echo "$branch" | sed 's/lint\/style-fixes-//') + # Validate branch name matches expected pattern + if [[ ! "$branch" =~ ^lint/style-fixes-[0-9]+$ ]]; then + echo " โš ๏ธ Warning: Branch name doesn't match expected pattern, skipping" + skipped_branches=$((skipped_branches + 1)) + echo "" + continue + fi # Check if there's a PR for this branch pr_number=$(gh pr list --state all --head "$branch" --json number --jq '.[0].number' 2>/dev/null || echo "") @@ -151,7 +159,7 @@ jobs: fi echo "" - done + done <<< "$lint_branches" # Print summary echo "๐Ÿ“Š Cleanup Summary" From a5939976b87132c8a5cb3e06f1f06dfc995c943b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:46:43 +0000 Subject: [PATCH 4/6] Improve error handling and logging clarity Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .github/workflows/cleanup-lint-branches.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cleanup-lint-branches.yml b/.github/workflows/cleanup-lint-branches.yml index c9a8582e..f3353c38 100644 --- a/.github/workflows/cleanup-lint-branches.yml +++ b/.github/workflows/cleanup-lint-branches.yml @@ -44,12 +44,13 @@ jobs: - name: Cleanup lint branches env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Default to false for manual triggers with dry_run=false, otherwise true - # This ensures scheduled runs default to true (safe mode) + # Default to true (dry-run mode) unless explicitly set to false + # This ensures safe operation for both scheduled and manual triggers DRY_RUN: ${{ github.event.inputs.dry_run == 'false' && 'false' || 'true' }} BRANCH_AGE_DAYS: 7 run: | set -e + set -o pipefail echo "๐Ÿงน Starting lint branch cleanup..." echo "Dry run mode: $DRY_RUN" @@ -148,11 +149,12 @@ jobs: deleted_branches=$((deleted_branches + 1)) else echo " ๐Ÿ—‘๏ธ Deleting: $delete_reason" - if gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/$branch" 2>/dev/null; then + delete_error=$(gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/$branch" 2>&1) + if [ $? -eq 0 ]; then echo " โœ… Successfully deleted" deleted_branches=$((deleted_branches + 1)) else - echo " โŒ Failed to delete branch" + echo " โŒ Failed to delete branch: $delete_error" error_branches=$((error_branches + 1)) fi fi From 0b55dc14c12b2929d6d6bea20ed90b2a457ac7a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:47:52 +0000 Subject: [PATCH 5/6] Use safer branch deletion and improved jq handling Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .github/workflows/cleanup-lint-branches.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cleanup-lint-branches.yml b/.github/workflows/cleanup-lint-branches.yml index f3353c38..f08af35c 100644 --- a/.github/workflows/cleanup-lint-branches.yml +++ b/.github/workflows/cleanup-lint-branches.yml @@ -69,7 +69,7 @@ jobs: # Get all remote branches matching the pattern lint/style-fixes-* echo "๐Ÿ“‹ Fetching lint branches..." - lint_branches=$(git ls-remote --heads origin 'refs/heads/lint/style-fixes-*' | awk '{print $2}' | sed 's#refs/heads/##' || echo "") + lint_branches=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/lint/style-fixes-*' | sed 's|^origin/||' || echo "") if [ -z "$lint_branches" ]; then echo "โœ… No lint branches found matching 'lint/style-fixes-*'" @@ -94,7 +94,7 @@ jobs: fi # Check if there's a PR for this branch - pr_number=$(gh pr list --state all --head "$branch" --json number --jq '.[0].number' 2>/dev/null || echo "") + pr_number=$(gh pr list --state all --head "$branch" --json number --jq 'if length > 0 then .[0].number else empty end' 2>/dev/null || echo "") should_delete=false delete_reason="" @@ -149,7 +149,8 @@ jobs: deleted_branches=$((deleted_branches + 1)) else echo " ๐Ÿ—‘๏ธ Deleting: $delete_reason" - delete_error=$(gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/$branch" 2>&1) + # Use git push for safer deletion with validated branch name + delete_error=$(git push origin --delete "$branch" 2>&1) if [ $? -eq 0 ]; then echo " โœ… Successfully deleted" deleted_branches=$((deleted_branches + 1)) From 38da7bfe5c8452d37bac7c751ee0a2c6c8c1c99c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:48:48 +0000 Subject: [PATCH 6/6] Simplify jq filter and properly capture exit codes Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .github/workflows/cleanup-lint-branches.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cleanup-lint-branches.yml b/.github/workflows/cleanup-lint-branches.yml index f08af35c..244565e5 100644 --- a/.github/workflows/cleanup-lint-branches.yml +++ b/.github/workflows/cleanup-lint-branches.yml @@ -94,7 +94,7 @@ jobs: fi # Check if there's a PR for this branch - pr_number=$(gh pr list --state all --head "$branch" --json number --jq 'if length > 0 then .[0].number else empty end' 2>/dev/null || echo "") + pr_number=$(gh pr list --state all --head "$branch" --json number --jq '.[0].number // empty' 2>/dev/null || echo "") should_delete=false delete_reason="" @@ -151,7 +151,8 @@ jobs: echo " ๐Ÿ—‘๏ธ Deleting: $delete_reason" # Use git push for safer deletion with validated branch name delete_error=$(git push origin --delete "$branch" 2>&1) - if [ $? -eq 0 ]; then + exit_code=$? + if [ $exit_code -eq 0 ]; then echo " โœ… Successfully deleted" deleted_branches=$((deleted_branches + 1)) else