From ba74039fce756728352d96ec092dd48032d6ed0b Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Wed, 11 Feb 2026 11:10:49 +0200 Subject: [PATCH] tools: cleanup .github/workflows/gh_restart_failed.v --- .github/workflows/gh_restart_failed.v | 242 ++++++++++---------------- 1 file changed, 88 insertions(+), 154 deletions(-) diff --git a/.github/workflows/gh_restart_failed.v b/.github/workflows/gh_restart_failed.v index 0351f951f..897f8c723 100644 --- a/.github/workflows/gh_restart_failed.v +++ b/.github/workflows/gh_restart_failed.v @@ -16,212 +16,146 @@ struct Check { workflow string } -struct GhRun { - database_id i64 @[json: databaseId] - workflow_name string @[json: workflowName] +fn (c Check) is_failed() bool { + return c.bucket == 'fail' || c.state in ['FAILURE', 'TIMED_OUT'] } -struct GhJob { - name string - status string - conclusion string - url string - database_id i64 @[json: databaseId] +fn (c Check) is_cancelled() bool { + return c.bucket == 'cancel' || c.state == 'CANCELLED' } -struct GhRunView { - jobs []GhJob - workflow_name string @[json: workflowName] +struct GhCheckRun { + name string + status string + conclusion string + html_url string @[json: html_url] +} + +struct GhCheckRunsResponse { + check_runs []GhCheckRun @[json: check_runs] } fn main() { unbuffer_stdout() - if os.args.len != 2 { + arg := os.args[1] or { println('Usage: v run gh_restart_failed.v ') return } - arg := os.args[1] - mut is_pr := true - if arg.len > 5 { - is_pr = false - } else { - for r in arg { - if !r.is_digit() { - is_pr = false - break - } - } - } + is_pr := arg.len < 6 && arg.bytes().all(it.is_digit()) + checks := if is_pr { get_checks_for_pr(arg.int()) } else { get_checks_for_commit(arg) } mut failed := []Check{} mut cancelled := []Check{} mut succeeded := 0 mut in_progress := 0 - mut total := 0 - checks := if is_pr { get_checks_for_pr(arg.int()) } else { get_checks_for_commit(arg) } for check in checks { - total++ - match check.bucket { - 'fail' { - failed << check - } - 'cancel' { - cancelled << check - } - 'pass' { - succeeded++ - } - 'pending' { - in_progress++ - } - else { - // Fallback to state if bucket is ambiguous - if check.state in ['FAILURE', 'TIMED_OUT'] { - failed << check - } else if check.state == 'CANCELLED' { - cancelled << check - } else if check.state == 'SUCCESS' { - succeeded++ - } else { - in_progress++ - } - } + if check.is_failed() { + failed << check + } else if check.is_cancelled() { + cancelled << check + } else if check.bucket == 'pass' || check.state == 'SUCCESS' { + succeeded++ + } else { + in_progress++ } } mut to_restart := []Check{} to_restart << failed to_restart << cancelled - // List failed/cancelled jobs + mut restarted_count := 0 if to_restart.len > 0 { println('Found ${to_restart.len} failed or cancelled jobs:') for job in to_restart { println('- ${job.workflow} / ${job.name} (${job.state})') } - } else { - println('No failed or cancelled jobs found.') - } - - mut restarted_count := 0 - // Ask for confirmation if there are jobs to restart - if to_restart.len > 0 { - println('') - print(c(tg, 'Do you want to restart these ${to_restart.len} jobs? [y/N]: ')) - answer := os.input('').to_lower().trim_space() - if answer == 'y' { + println('\n' + c(tg, 'Do you want to restart these ${to_restart.len} jobs? [y/N]: ')) + if os.input('').to_lower().trim_space() == 'y' { println('') for job in to_restart { - // Extract run_id and job_id from link - // Link format: https://.../runs//job/ - parts := job.link.split('/') - runs_idx := parts.index('runs') - job_kw_idx := parts.index('job') - mut run_id := '' - mut job_id := '' - if runs_idx != -1 && runs_idx + 1 < parts.len { - run_id = parts[runs_idx + 1] - } - if job_kw_idx != -1 && job_kw_idx + 1 < parts.len { - job_id = parts[job_kw_idx + 1] - } + run_id, job_id := parse_ids(job.link) if run_id == '' || job_id == '' { println('Could not parse IDs from link: ${job.link} (Skipping)') continue } print('Restarting ${job.name} (Run: ${run_id}, Job: ${job_id})... ') - // Attempt restart - // Using --job with run_id - restart_cmd := 'gh run rerun ${run_id} --job ${job_id}' - restart_res := execute_with_progress(restart_cmd) - if restart_res.exit_code == 0 { + res := execute_with_progress('gh run rerun ${run_id} --job ${job_id}') + if res.exit_code == 0 { restarted_count++ } else { - println(' Error: ${restart_res.output.trim_space()}') + println(' Error: ${res.output.trim_space()}') } } } else { println('Aborted restart.') } + } else { + println('No failed or cancelled jobs found.') } - // Final Summary - println('') - println(c(tg, 'Summary:')) - println('Total jobs found: ${m(total)}') - println('Failed: ${m(failed.len)}') - println('Cancelled: ${m(cancelled.len)}') - println('Succeeded: ${m(succeeded)}') - println('In Progress: ${m(in_progress)}') - println('Restarted: ${m(restarted_count)}') + println('\n' + c(tg, 'Summary:')) + println('Total jobs: ${m(checks.len)}') + println('Failed: ${m(failed.len)}') + println('Cancelled: ${m(cancelled.len)}') + println('Succeeded: ${m(succeeded)}') + println('In Progress: ${m(in_progress)}') + println('Restarted: ${m(restarted_count)}') +} + +fn m(n int) string { + return c(tb, n.str()) } -fn m(metric int) string { - return c(tb, metric.str()) +fn parse_ids(link string) (string, string) { + parts := link.split('/') + runs_idx := parts.index('runs') + job_idx := parts.index('job') + mut run_id := '' + mut job_id := '' + if runs_idx != -1 && runs_idx + 1 < parts.len { + run_id = parts[runs_idx + 1] + } + if job_idx != -1 && job_idx + 1 < parts.len { + job_id = parts[job_idx + 1] + } + return run_id, job_id } fn get_checks_for_pr(pr_number int) []Check { - mut checks := []Check{} println(c(tg, 'Fetching checks for PR ${m(pr_number)}...')) - cmd := 'gh pr checks ${pr_number} --json name,bucket,state,link,workflow' - res := execute_with_progress(cmd) + res := execute_with_progress('gh pr checks ${pr_number} --json name,bucket,state,link,workflow') if res.exit_code != 0 { - println('Error fetching checks: ${res.output}') - exit(1) - } - checks = json.decode([]Check, res.output) or { - println('Failed to decode JSON: ${err}') + println('Error: ${res.output}') exit(1) } - return checks -} - -struct GhCheckRun { - name string - status string - conclusion string - html_url string @[json: html_url] -} - -struct GhCheckRunsResponse { - total_count int @[json: total_count] - check_runs []GhCheckRun @[json: check_runs] + return json.decode([]Check, res.output) or { exit(1) } } fn get_checks_for_commit(commit string) []Check { - mut checks := []Check{} println(c(tg, 'Fetching checks for ref ${c(tb, commit)}...')) - // Fetch all check runs for this commit in one go - cmd := 'gh api repos/:owner/:repo/commits/${commit}/check-runs?per_page=100' - res := execute_with_progress(cmd) + res := execute_with_progress('gh api repos/:owner/:repo/commits/${commit}/check-runs?per_page=100') if res.exit_code != 0 { - println('Error fetching check runs: ${res.output}') + println('Error: ${res.output}') exit(1) } - response := json.decode(GhCheckRunsResponse, res.output) or { - println('Failed to decode check runs JSON: ${err}') - exit(1) - } - for cr in response.check_runs { - mut bucket := 'pass' - mut state := cr.conclusion.to_upper() - if state == '' { - state = cr.status.to_upper() - } - if cr.conclusion == 'failure' { - bucket = 'fail' - } else if cr.conclusion == 'cancelled' { - bucket = 'cancel' - } else if cr.status in ['in_progress', 'queued', 'waiting'] { - bucket = 'pending' - if state == '' { - state = 'PENDING' - } - } - // The API doesn't directly give workflow name, but it's often in the name - // or we can just use the job name for both if it's not available. + resp := json.decode(GhCheckRunsResponse, res.output) or { exit(1) } + mut checks := []Check{} + for cr in resp.check_runs { checks << Check{ name: cr.name - bucket: bucket - state: state + bucket: if cr.conclusion == 'failure' { + 'fail' + } else if cr.conclusion == 'cancelled' { + 'cancel' + } else if cr.conclusion == 'success' { + 'pass' + } else { + 'pending' + } + state: if cr.conclusion != '' { + cr.conclusion.to_upper() + } else { + cr.status.to_upper() + } link: cr.html_url - workflow: 'Actions' // Default if unknown + workflow: 'Actions' } } return checks @@ -229,22 +163,22 @@ fn get_checks_for_commit(commit string) []Check { fn execute_with_progress(cmd string) os.Result { start := time.now() - start_str := start.hhmmss() mut stop := false - spawn fn (cmd string, start_str string, start time.Time, mut stop &bool) { + spawn fn (cmd string, start time.Time, mut stop &bool) { + start_str := start.hhmmss() for !*stop { elapsed := time.since(start).seconds() - print('\rRunning ${c(tm, cmd)} [started at ${start_str}] ... elapsed ${elapsed:.1f}s') + print('\rRunning ${c(tm, cmd)} [${start_str}] ... ${elapsed:.1f}s') os.flush() time.sleep(100 * time.millisecond) } - }(cmd, start_str, start, mut &stop) - + }(cmd, start, mut &stop) res := os.execute(cmd) - stop = true - time.sleep(150 * time.millisecond) elapsed := time.since(start).seconds() + stop = true + time.sleep(100 * time.millisecond) status := if res.exit_code == 0 { 'OK' } else { 'Failed' } - println('\rCommand ${c(tm, cmd)} [started at ${start_str}] ... done in ${elapsed:.1f}s. Status: ${status}.') + print('\r') + println('Command ${c(tm, cmd)} done in ${elapsed:.1f}s. ${status}') return res } -- 2.39.5