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}"