Add first-class Actions CLI support
This commit is contained in:
302
crates/cli/src/actions.rs
Normal file
302
crates/cli/src/actions.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<RepositoryScope, Box<dyn Error>> {
|
||||
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<i64, Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user