| 1 | import os |
| 2 | import json |
| 3 | import term |
| 4 | import time |
| 5 | |
| 6 | const c = term.colorize |
| 7 | const tg = term.green |
| 8 | const tm = term.magenta |
| 9 | const tb = term.bold |
| 10 | |
| 11 | struct Check { |
| 12 | name string |
| 13 | bucket string |
| 14 | state string |
| 15 | link string |
| 16 | workflow string |
| 17 | } |
| 18 | |
| 19 | fn (c Check) is_failed() bool { |
| 20 | return c.bucket == 'fail' || c.state in ['FAILURE', 'TIMED_OUT'] |
| 21 | } |
| 22 | |
| 23 | fn (c Check) is_cancelled() bool { |
| 24 | return c.bucket == 'cancel' || c.state == 'CANCELLED' |
| 25 | } |
| 26 | |
| 27 | struct GhCheckRun { |
| 28 | name string |
| 29 | status string |
| 30 | conclusion string |
| 31 | html_url string @[json: html_url] |
| 32 | } |
| 33 | |
| 34 | struct GhCheckRunsResponse { |
| 35 | check_runs []GhCheckRun @[json: check_runs] |
| 36 | } |
| 37 | |
| 38 | fn 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 | |
| 102 | fn m(n int) string { |
| 103 | return c(tb, n.str()) |
| 104 | } |
| 105 | |
| 106 | fn 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 | |
| 121 | fn 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 | |
| 131 | fn 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 | |
| 164 | fn 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 | |