use std::error::Error; use gotcha_gitea::{ ActionJobLog, ActionRunDetails, Client, models::{ActionWorkflow, ActionWorkflowJob, ActionWorkflowRun}, }; use crate::{ config::{RepositoryScope, Selection}, print_table, }; const ACTION_HELP: &str = "\ Usage: gotcha action workflow|run SUBCOMMAND Workflow subcommands: workflow list [OWNER/REPOSITORY] List workflows workflow dispatch WORKFLOW REF [OWNER/REPOSITORY] Dispatch a workflow on a Git reference Run subcommands: run list [OWNER/REPOSITORY] List workflow runs run show ID [OWNER/REPOSITORY] Show a run and its jobs run logs ID [OWNER/REPOSITORY] Print every job log for a run"; const WORKFLOW_HELP: &str = "\ Usage: gotcha action workflow SUBCOMMAND Subcommands: list [OWNER/REPOSITORY] dispatch WORKFLOW REF [OWNER/REPOSITORY]"; const RUN_HELP: &str = "\ Usage: gotcha action run SUBCOMMAND Subcommands: list [OWNER/REPOSITORY] show ID [OWNER/REPOSITORY] logs ID [OWNER/REPOSITORY]"; pub async fn run( command: &[String], selection: &Selection, client: &Client, ) -> Result> { match command { [domain, group, action] if domain == "action" && group == "workflow" && action == "list" => { print_workflows(&client.action_workflows(&scope(selection, None)?).await?); } [domain, group, action, repository] if domain == "action" && group == "workflow" && action == "list" => { print_workflows( &client .action_workflows(&scope(selection, Some(repository))?) .await?, ); } [domain, group, action, workflow, reference] if domain == "action" && group == "workflow" && action == "dispatch" => { dispatch(client, &scope(selection, None)?, workflow, reference).await?; } [domain, group, action, workflow, reference, repository] if domain == "action" && group == "workflow" && action == "dispatch" => { dispatch( client, &scope(selection, Some(repository))?, workflow, reference, ) .await?; } [domain, group, action] if domain == "action" && group == "run" && action == "list" => { print_runs(&client.action_runs(&scope(selection, None)?).await?); } [domain, group, action, repository] if domain == "action" && group == "run" && action == "list" => { print_runs( &client .action_runs(&scope(selection, Some(repository))?) .await?, ); } [domain, group, action, id] if domain == "action" && group == "run" && action == "show" => { print_run( &client .action_run_details(&scope(selection, None)?, number(id)?) .await?, ); } [domain, group, action, id, repository] if domain == "action" && group == "run" && action == "show" => { print_run( &client .action_run_details(&scope(selection, Some(repository))?, number(id)?) .await?, ); } [domain, group, action, id] if domain == "action" && group == "run" && action == "logs" => { print_logs( number(id)?, &client .action_run_logs(&scope(selection, None)?, number(id)?) .await?, ); } [domain, group, action, id, repository] if domain == "action" && group == "run" && action == "logs" => { print_logs( number(id)?, &client .action_run_logs(&scope(selection, Some(repository))?, number(id)?) .await?, ); } [domain, ..] if domain == "action" => return Ok(false), _ => return Ok(false), } Ok(true) } pub fn domain_help(domain: &str) -> Option<&'static str> { (domain == "action").then_some(ACTION_HELP) } pub fn subcommand_help(domain: &str, command: &str) -> Option<&'static str> { match (domain, command) { ("action", "workflow") => Some(WORKFLOW_HELP), ("action", "run") => Some(RUN_HELP), _ => None, } } fn scope(selection: &Selection, value: Option<&str>) -> Result> { match value { Some(value) => Ok(RepositoryScope::parse(value)?), None => selection.repository.clone().ok_or_else(|| { "cannot infer a repository; run inside one or pass OWNER/REPOSITORY".into() }), } } fn number(value: &str) -> Result> { let number = value.parse()?; if number < 1 { return Err("run ID must be a positive integer".into()); } Ok(number) } async fn dispatch( client: &Client, repository: &RepositoryScope, workflow: &str, reference: &str, ) -> Result<(), Box> { client .dispatch_action_workflow(repository, workflow, reference) .await?; println!("Dispatched workflow {workflow} on {reference}."); Ok(()) } fn print_workflows(workflows: &[ActionWorkflow]) { print_table( &[("ID", 40), ("STATE", 12), ("NAME", 50), ("PATH", 70)], workflows .iter() .map(|workflow| { vec![ workflow.id.as_deref().unwrap_or("unknown").into(), workflow.state.as_deref().unwrap_or("unknown").into(), workflow.name.as_deref().unwrap_or("").into(), workflow.path.as_deref().unwrap_or("").into(), ] }) .collect(), ); } fn print_runs(runs: &[ActionWorkflowRun]) { print_table( &[ ("ID", 12), ("RUN", 8), ("STATUS", 14), ("RESULT", 14), ("EVENT", 20), ("BRANCH", 30), ("WORKFLOW", 60), ], runs.iter() .map(|run| { vec![ run.id.unwrap_or_default().to_string(), run.run_number.unwrap_or_default().to_string(), run.status.as_deref().unwrap_or("unknown").into(), run.conclusion.as_deref().unwrap_or("-").into(), run.event.as_deref().unwrap_or("unknown").into(), run.head_branch.as_deref().unwrap_or("-").into(), run.path.as_deref().unwrap_or("").into(), ] }) .collect(), ); } fn print_run(details: &ActionRunDetails) { let run = &details.run; println!( "Run {} #{} [{} / {}] {}", run.id.unwrap_or_default(), run.run_number.unwrap_or_default(), run.status.as_deref().unwrap_or("unknown"), run.conclusion.as_deref().unwrap_or("-"), run.display_title.as_deref().unwrap_or("") ); field("workflow", run.path.as_deref()); field("event", run.event.as_deref()); field("branch", run.head_branch.as_deref()); field("commit", run.head_sha.as_deref()); field("started", run.started_at.as_deref()); field("completed", run.completed_at.as_deref()); field("url", run.html_url.as_deref()); print_jobs(&details.jobs); } fn print_jobs(jobs: &[ActionWorkflowJob]) { println!(); print_table( &[ ("JOB", 12), ("STATUS", 14), ("RESULT", 14), ("RUNNER", 30), ("NAME", 60), ], jobs.iter() .map(|job| { vec![ job.id.unwrap_or_default().to_string(), job.status.as_deref().unwrap_or("unknown").into(), job.conclusion.as_deref().unwrap_or("-").into(), job.runner_name.as_deref().unwrap_or("-").into(), job.name.as_deref().unwrap_or("").into(), ] }) .collect(), ); } fn print_logs(run: i64, logs: &[ActionJobLog]) { if logs.is_empty() { println!("No jobs found for run {run}."); return; } for (index, log) in logs.iter().enumerate() { if index > 0 { println!(); } println!( "== {} ({}) ==", log.job.name.as_deref().unwrap_or("job"), log.job.id.unwrap_or_default() ); print!("{}", log.text); if !log.text.ends_with('\n') { println!(); } } } fn field(name: &str, value: Option<&str>) { if let Some(value) = value.filter(|value| !value.is_empty()) { println!("{name}: {value}"); } } #[cfg(test)] mod tests { use super::*; #[test] fn validates_run_ids_and_exposes_nested_help() { assert!(number("0").is_err()); assert_eq!(number("95").unwrap(), 95); assert!(domain_help("action").unwrap().contains("workflow dispatch")); assert!( subcommand_help("action", "run") .unwrap() .contains("logs ID") ); } }