Skip to content

Commit 7ad51d4

Browse files
committed
tools: add .github/workflows/gh_restart_failed.v to help recover after github system failures
1 parent 586d34d commit 7ad51d4

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import os
2+
import json
3+
import term
4+
5+
const c = term.colorize
6+
const tg = term.green
7+
const tb = term.bold
8+
9+
struct Check {
10+
name string
11+
bucket string
12+
state string
13+
link string
14+
workflow string
15+
}
16+
17+
fn main() {
18+
unbuffer_stdout()
19+
if os.args.len != 2 {
20+
println('Usage: v run gh_restart_failed.v <PR_NUMBER>')
21+
return
22+
}
23+
pr_number := os.args[1].int()
24+
println(c(tg, 'Fetching checks for PR ${m(pr_number)}...'))
25+
// Fetch checks using gh CLI
26+
cmd := 'gh pr checks ${pr_number} --json name,bucket,state,link,workflow'
27+
res := os.execute(cmd)
28+
if res.exit_code != 0 {
29+
println('Error fetching checks: ${res.output}')
30+
return
31+
}
32+
checks := json.decode([]Check, res.output) or {
33+
println('Failed to decode JSON: ${err}')
34+
return
35+
}
36+
mut failed := []Check{}
37+
mut cancelled := []Check{}
38+
mut succeeded := 0
39+
mut in_progress := 0
40+
mut total := 0
41+
for check in checks {
42+
total++
43+
match check.bucket {
44+
'fail' {
45+
failed << check
46+
}
47+
'cancel' {
48+
cancelled << check
49+
}
50+
'pass' {
51+
succeeded++
52+
}
53+
'pending' {
54+
in_progress++
55+
}
56+
else {
57+
// Fallback to state if bucket is ambiguous
58+
if check.state in ['FAILURE', 'TIMED_OUT'] {
59+
failed << check
60+
} else if check.state == 'CANCELLED' {
61+
cancelled << check
62+
} else if check.state == 'SUCCESS' {
63+
succeeded++
64+
} else {
65+
in_progress++
66+
}
67+
}
68+
}
69+
}
70+
mut to_restart := []Check{}
71+
to_restart << failed
72+
to_restart << cancelled
73+
// List failed/cancelled jobs
74+
if to_restart.len > 0 {
75+
println('Found ${to_restart.len} failed or cancelled jobs:')
76+
for job in to_restart {
77+
println('- ${job.workflow} / ${job.name} (${job.state})')
78+
}
79+
} else {
80+
println('No failed or cancelled jobs found.')
81+
}
82+
83+
mut restarted_count := 0
84+
// Ask for confirmation if there are jobs to restart
85+
if to_restart.len > 0 {
86+
println('')
87+
print(c(tg, 'Do you want to restart these ${to_restart.len} jobs? [y/N]: '))
88+
answer := os.input('').to_lower().trim_space()
89+
if answer == 'y' {
90+
println('')
91+
for job in to_restart {
92+
// Extract run_id and job_id from link
93+
// Link format: https://.../runs/<run_id>/job/<job_id>
94+
parts := job.link.split('/')
95+
runs_idx := parts.index('runs')
96+
job_kw_idx := parts.index('job')
97+
mut run_id := ''
98+
mut job_id := ''
99+
if runs_idx != -1 && runs_idx + 1 < parts.len {
100+
run_id = parts[runs_idx + 1]
101+
}
102+
if job_kw_idx != -1 && job_kw_idx + 1 < parts.len {
103+
job_id = parts[job_kw_idx + 1]
104+
}
105+
if run_id == '' || job_id == '' {
106+
println('Could not parse IDs from link: ${job.link} (Skipping)')
107+
continue
108+
}
109+
print('Restarting ${job.name} (Run: ${run_id}, Job: ${job_id})... ')
110+
// Attempt restart
111+
// Using --job <job_id> with run_id
112+
restart_cmd := 'gh run rerun ${run_id} --job ${job_id}'
113+
restart_res := os.execute(restart_cmd)
114+
if restart_res.exit_code == 0 {
115+
println('OK')
116+
restarted_count++
117+
} else {
118+
println('Failed')
119+
println(' Error: ${restart_res.output.trim_space()}')
120+
}
121+
}
122+
} else {
123+
println('Aborted restart.')
124+
}
125+
}
126+
// Final Summary
127+
println('')
128+
println(c(tg, 'Summary:'))
129+
println('Total jobs found: ${m(total)}')
130+
println('Failed: ${m(failed.len)}')
131+
println('Cancelled: ${m(cancelled.len)}')
132+
println('Succeeded: ${m(succeeded)}')
133+
println('In Progress: ${m(in_progress)}')
134+
println('Restarted: ${m(restarted_count)}')
135+
}
136+
137+
fn m(metric int) string {
138+
return c(tb, metric.str())
139+
}

0 commit comments

Comments
 (0)