v / .github / workflows / gh_restart_failed.v
184 lines · 172 sloc · 4.67 KB · ba74039fce756728352d96ec092dd48032d6ed0b
Raw
1import os
2import json
3import term
4import time
5
6const c = term.colorize
7const tg = term.green
8const tm = term.magenta
9const tb = term.bold
10
11struct Check {
12 name string
13 bucket string
14 state string
15 link string
16 workflow string
17}
18
19fn (c Check) is_failed() bool {
20 return c.bucket == 'fail' || c.state in ['FAILURE', 'TIMED_OUT']
21}
22
23fn (c Check) is_cancelled() bool {
24 return c.bucket == 'cancel' || c.state == 'CANCELLED'
25}
26
27struct GhCheckRun {
28 name string
29 status string
30 conclusion string
31 html_url string @[json: html_url]
32}
33
34struct GhCheckRunsResponse {
35 check_runs []GhCheckRun @[json: check_runs]
36}
37
38fn main() {
39 unbuffer_stdout()
40 arg := os.args[1] or {
41 println('Usage: v run gh_restart_failed.v <PR_NUMBER|REF>')
42 return
43 }
44 is_pr := arg.len < 6 && arg.bytes().all(it.is_digit())
45 checks := if is_pr { get_checks_for_pr(arg.int()) } else { get_checks_for_commit(arg) }
46 mut failed := []Check{}
47 mut cancelled := []Check{}
48 mut succeeded := 0
49 mut in_progress := 0
50 for check in checks {
51 if check.is_failed() {
52 failed << check
53 } else if check.is_cancelled() {
54 cancelled << check
55 } else if check.bucket == 'pass' || check.state == 'SUCCESS' {
56 succeeded++
57 } else {
58 in_progress++
59 }
60 }
61 mut to_restart := []Check{}
62 to_restart << failed
63 to_restart << cancelled
64 mut restarted_count := 0
65 if to_restart.len > 0 {
66 println('Found ${to_restart.len} failed or cancelled jobs:')
67 for job in to_restart {
68 println('- ${job.workflow} / ${job.name} (${job.state})')
69 }
70 println('\n' + c(tg, 'Do you want to restart these ${to_restart.len} jobs? [y/N]: '))
71 if os.input('').to_lower().trim_space() == 'y' {
72 println('')
73 for job in to_restart {
74 run_id, job_id := parse_ids(job.link)
75 if run_id == '' || job_id == '' {
76 println('Could not parse IDs from link: ${job.link} (Skipping)')
77 continue
78 }
79 print('Restarting ${job.name} (Run: ${run_id}, Job: ${job_id})... ')
80 res := execute_with_progress('gh run rerun ${run_id} --job ${job_id}')
81 if res.exit_code == 0 {
82 restarted_count++
83 } else {
84 println(' Error: ${res.output.trim_space()}')
85 }
86 }
87 } else {
88 println('Aborted restart.')
89 }
90 } else {
91 println('No failed or cancelled jobs found.')
92 }
93 println('\n' + c(tg, 'Summary:'))
94 println('Total jobs: ${m(checks.len)}')
95 println('Failed: ${m(failed.len)}')
96 println('Cancelled: ${m(cancelled.len)}')
97 println('Succeeded: ${m(succeeded)}')
98 println('In Progress: ${m(in_progress)}')
99 println('Restarted: ${m(restarted_count)}')
100}
101
102fn m(n int) string {
103 return c(tb, n.str())
104}
105
106fn parse_ids(link string) (string, string) {
107 parts := link.split('/')
108 runs_idx := parts.index('runs')
109 job_idx := parts.index('job')
110 mut run_id := ''
111 mut job_id := ''
112 if runs_idx != -1 && runs_idx + 1 < parts.len {
113 run_id = parts[runs_idx + 1]
114 }
115 if job_idx != -1 && job_idx + 1 < parts.len {
116 job_id = parts[job_idx + 1]
117 }
118 return run_id, job_id
119}
120
121fn get_checks_for_pr(pr_number int) []Check {
122 println(c(tg, 'Fetching checks for PR ${m(pr_number)}...'))
123 res := execute_with_progress('gh pr checks ${pr_number} --json name,bucket,state,link,workflow')
124 if res.exit_code != 0 {
125 println('Error: ${res.output}')
126 exit(1)
127 }
128 return json.decode([]Check, res.output) or { exit(1) }
129}
130
131fn get_checks_for_commit(commit string) []Check {
132 println(c(tg, 'Fetching checks for ref ${c(tb, commit)}...'))
133 res := execute_with_progress('gh api repos/:owner/:repo/commits/${commit}/check-runs?per_page=100')
134 if res.exit_code != 0 {
135 println('Error: ${res.output}')
136 exit(1)
137 }
138 resp := json.decode(GhCheckRunsResponse, res.output) or { exit(1) }
139 mut checks := []Check{}
140 for cr in resp.check_runs {
141 checks << Check{
142 name: cr.name
143 bucket: if cr.conclusion == 'failure' {
144 'fail'
145 } else if cr.conclusion == 'cancelled' {
146 'cancel'
147 } else if cr.conclusion == 'success' {
148 'pass'
149 } else {
150 'pending'
151 }
152 state: if cr.conclusion != '' {
153 cr.conclusion.to_upper()
154 } else {
155 cr.status.to_upper()
156 }
157 link: cr.html_url
158 workflow: 'Actions'
159 }
160 }
161 return checks
162}
163
164fn execute_with_progress(cmd string) os.Result {
165 start := time.now()
166 mut stop := false
167 spawn fn (cmd string, start time.Time, mut stop &bool) {
168 start_str := start.hhmmss()
169 for !*stop {
170 elapsed := time.since(start).seconds()
171 print('\rRunning ${c(tm, cmd)} [${start_str}] ... ${elapsed:.1f}s')
172 os.flush()
173 time.sleep(100 * time.millisecond)
174 }
175 }(cmd, start, mut &stop)
176 res := os.execute(cmd)
177 elapsed := time.since(start).seconds()
178 stop = true
179 time.sleep(100 * time.millisecond)
180 status := if res.exit_code == 0 { 'OK' } else { 'Failed' }
181 print('\r')
182 println('Command ${c(tm, cmd)} done in ${elapsed:.1f}s. ${status}')
183 return res
184}
185