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