nftable-migration
  1#!/usr/bin/env bash
  2
  3set -euo pipefail
  4
  5# Colors for output
  6RED='\033[0;31m'
  7GREEN='\033[0;32m'
  8YELLOW='\033[1;33m'
  9BLUE='\033[0;34m'
 10NC='\033[0m' # No Color
 11
 12# Help message
 13usage() {
 14    cat <<EOF
 15Usage: gh-restart-failed [OPTIONS] [REPOSITORY]
 16
 17List pull requests with failed checks and restart selected workflows.
 18
 19Options:
 20    -i, --ignore PATTERN    Ignore workflows matching PATTERN (can be used multiple times)
 21    -l, --label LABEL       Filter PRs by label (can be used multiple times)
 22    -h, --help             Show this help message
 23
 24Arguments:
 25    REPOSITORY    Optional repository in OWNER/REPO format or path to local repo.
 26                  If not provided, uses the current directory's repository.
 27
 28Dependencies:
 29    - gh (GitHub CLI)
 30    - fzf (fuzzy finder)
 31    - jq (JSON processor)
 32
 33Note:
 34    By default, "Label Checker" workflows are ignored. Use -i to add more patterns.
 35
 36Examples:
 37    gh-restart-failed                                    # Use current repository
 38    gh-restart-failed owner/repo                         # Use specific GitHub repository
 39    gh-restart-failed -i "build" -i "test"              # Ignore build and test workflows
 40    gh-restart-failed -l "bug" -l "enhancement"         # Only show PRs with bug OR enhancement labels
 41    gh-restart-failed /path/to/repo                     # Use repository at path
 42
 43EOF
 44    exit 0
 45}
 46
 47# Check dependencies
 48check_dependencies() {
 49    local missing=()
 50
 51    for cmd in gh fzf jq; do
 52        if ! command -v "$cmd" &> /dev/null; then
 53            missing+=("$cmd")
 54        fi
 55    done
 56
 57    if [ ${#missing[@]} -gt 0 ]; then
 58        echo -e "${RED}Error: Missing required dependencies: ${missing[*]}${NC}" >&2
 59        echo "Please install them and try again." >&2
 60        exit 1
 61    fi
 62}
 63
 64# Default ignore patterns
 65IGNORE_PATTERNS=("Label Checker")
 66LABEL_FILTERS=()
 67
 68# Parse arguments
 69REPO_ARG=""
 70while [[ $# -gt 0 ]]; do
 71    case $1 in
 72        -h|--help)
 73            usage
 74            ;;
 75        -i|--ignore)
 76            if [ -n "${2:-}" ]; then
 77                IGNORE_PATTERNS+=("$2")
 78                shift 2
 79            else
 80                echo -e "${RED}Error: --ignore requires a pattern argument${NC}" >&2
 81                exit 1
 82            fi
 83            ;;
 84        -l|--label)
 85            if [ -n "${2:-}" ]; then
 86                LABEL_FILTERS+=("$2")
 87                shift 2
 88            else
 89                echo -e "${RED}Error: --label requires a label argument${NC}" >&2
 90                exit 1
 91            fi
 92            ;;
 93        -*)
 94            echo -e "${RED}Error: Unknown option: $1${NC}" >&2
 95            usage
 96            ;;
 97        *)
 98            REPO_ARG="$1"
 99            shift
100            ;;
101    esac
102done
103
104check_dependencies
105
106# Determine repository context
107REPO_FLAG=()
108if [ -n "$REPO_ARG" ]; then
109    if [ -d "$REPO_ARG" ]; then
110        # It's a directory path
111        REPO_FLAG=(-R "$(cd "$REPO_ARG" && gh repo view --json nameWithOwner -q .nameWithOwner)")
112    else
113        # Assume it's OWNER/REPO format
114        REPO_FLAG=(-R "$REPO_ARG")
115    fi
116fi
117
118# Show ignored patterns
119if [ ${#IGNORE_PATTERNS[@]} -gt 0 ]; then
120    echo -e "${YELLOW}Ignoring workflows matching: ${IGNORE_PATTERNS[*]}${NC}" >&2
121fi
122
123# Show label filters
124if [ ${#LABEL_FILTERS[@]} -gt 0 ]; then
125    echo -e "${YELLOW}Filtering PRs with labels: ${LABEL_FILTERS[*]}${NC}" >&2
126fi
127
128# Get all open PRs with their check status
129echo -e "${BLUE}Fetching pull requests...${NC}" >&2
130
131# Build label filter arguments for gh pr list
132LABEL_ARGS=()
133for label in "${LABEL_FILTERS[@]}"; do
134    LABEL_ARGS+=(--label "$label")
135done
136
137# Fetch PRs with detailed check information
138prs_json=$(gh pr list "${REPO_FLAG[@]}" \
139    "${LABEL_ARGS[@]}" \
140    --json number,title,headRefName,author,statusCheckRollup \
141    --limit 100)
142
143# Filter PRs with failed checks and format for display
144failed_prs=$(echo "$prs_json" | jq -r '
145    .[] |
146    select(.statusCheckRollup // [] | any(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "STARTUP_FAILURE" or .conclusion == "ACTION_REQUIRED")) |
147    {
148        number: .number,
149        title: .title,
150        branch: .headRefName,
151        author: .author.login,
152        failed_checks: [.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "STARTUP_FAILURE" or .conclusion == "ACTION_REQUIRED")]
153    } |
154    "#\(.number) | \(.title) | @\(.author) | \(.branch) | \(.failed_checks | length) failed"
155')
156
157if [ -z "$failed_prs" ]; then
158    echo -e "${GREEN}No pull requests with failed checks found!${NC}"
159    exit 0
160fi
161
162echo -e "${YELLOW}Found pull requests with failed checks:${NC}" >&2
163echo ""
164
165# Use fzf to select PRs
166selected_prs=$(echo "$failed_prs" | fzf \
167    --multi \
168    --ansi \
169    --header="Select pull requests to restart failed workflows (TAB to select multiple, ENTER to confirm)" \
170    --preview="pr_number=\$(echo {} | cut -d'|' -f1 | tr -d '# '); gh pr checks ${REPO_FLAG[*]} \"\$pr_number\" 2>/dev/null | grep -E '(fail|FAILURE|×)' || echo 'Loading...'" \
171    --preview-window=right:60%:wrap \
172    --bind='ctrl-/:toggle-preview' \
173    --height=80%)
174
175if [ -z "$selected_prs" ]; then
176    echo -e "${YELLOW}No pull requests selected.${NC}"
177    exit 0
178fi
179
180echo ""
181echo -e "${BLUE}Processing selected pull requests...${NC}"
182echo ""
183
184# Process each selected PR
185while IFS= read -r pr_line; do
186    pr_number=$(echo "$pr_line" | cut -d'|' -f1 | tr -d '# ' | xargs)
187    pr_title=$(echo "$pr_line" | cut -d'|' -f2 | xargs)
188    pr_branch=$(echo "$pr_line" | cut -d'|' -f4 | xargs)
189
190    echo -e "${BLUE}PR #$pr_number: $pr_title${NC}"
191
192    # Build jq ignore filter
193    ignore_filter=""
194    for pattern in "${IGNORE_PATTERNS[@]}"; do
195        if [ -n "$ignore_filter" ]; then
196            ignore_filter="$ignore_filter and "
197        fi
198        ignore_filter="${ignore_filter}(.name | contains(\"$pattern\") | not)"
199    done
200
201    # Get failed workflow runs for this PR using the branch
202    failed_runs=$(gh run list "${REPO_FLAG[@]}" \
203        --branch "$pr_branch" \
204        --json databaseId,name,conclusion,status,event \
205        --limit 50 \
206        | jq -r "
207        .[] |
208        select(.event == \"pull_request\" and (.conclusion == \"failure\" or .conclusion == \"timed_out\" or .conclusion == \"startup_failure\" or .conclusion == \"action_required\") and ($ignore_filter)) |
209        \"\(.databaseId)|\(.name)|\(.conclusion)\"")
210
211    if [ -z "$failed_runs" ]; then
212        echo -e "${YELLOW}  No failed workflow runs found (may have been restarted already)${NC}"
213        continue
214    fi
215
216    # Restart all failed workflow runs
217    echo -e "${YELLOW}  Restarting failed workflows:${NC}"
218
219    echo "$failed_runs" | while IFS='|' read -r run_id workflow_name status; do
220        echo -e "  ${GREEN}${NC} Restarting: $workflow_name ($status)"
221
222        rerun_output=$(gh run rerun "${REPO_FLAG[@]}" "$run_id" --failed 2>&1)
223
224        if echo "$rerun_output" | grep -q "created over a month ago"; then
225            echo -e "    ${YELLOW}${NC} Cannot restart: workflow run is too old (>1 month)"
226        elif echo "$rerun_output" | grep -qi "error"; then
227            echo -e "    ${RED}${NC} Failed to restart: $rerun_output"
228        else
229            echo -e "    ${GREEN}${NC} Restarted successfully"
230        fi
231    done
232
233    echo ""
234done <<< "$selected_prs"
235
236echo -e "${GREEN}Done!${NC}"