Add first-class Actions management (#63)
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
use std::error::Error;
|
||||
|
||||
use gotcha_gitea::{
|
||||
ActionJobLog, ActionRunDetails, Client,
|
||||
ActionJobLog, ActionRunDetails, ActionRunQuery, Client,
|
||||
models::{ActionWorkflow, ActionWorkflowJob, ActionWorkflowRun},
|
||||
parse_action_inputs,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -16,11 +17,14 @@ Usage: gotcha action workflow|run SUBCOMMAND
|
||||
Workflow subcommands:
|
||||
workflow list [OWNER/REPOSITORY]
|
||||
List workflows
|
||||
workflow dispatch WORKFLOW REF [OWNER/REPOSITORY]
|
||||
workflow dispatch WORKFLOW REF [OWNER/REPOSITORY] [--input KEY=VALUE]…
|
||||
Dispatch a workflow on a Git reference
|
||||
|
||||
Run subcommands:
|
||||
run list [OWNER/REPOSITORY] List workflow runs
|
||||
run list [OWNER/REPOSITORY] [--page N] [--limit N]
|
||||
[--status STATUS] [--event EVENT] [--branch BRANCH]
|
||||
[--actor LOGIN] [--sha SHA]
|
||||
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";
|
||||
|
||||
@@ -29,13 +33,14 @@ Usage: gotcha action workflow SUBCOMMAND
|
||||
|
||||
Subcommands:
|
||||
list [OWNER/REPOSITORY]
|
||||
dispatch WORKFLOW REF [OWNER/REPOSITORY]";
|
||||
dispatch WORKFLOW REF [OWNER/REPOSITORY] [--input KEY=VALUE]…";
|
||||
|
||||
const RUN_HELP: &str = "\
|
||||
Usage: gotcha action run SUBCOMMAND
|
||||
|
||||
Subcommands:
|
||||
list [OWNER/REPOSITORY]
|
||||
list [OWNER/REPOSITORY] [--page N] [--limit N] [--status STATUS]
|
||||
[--event EVENT] [--branch BRANCH] [--actor LOGIN] [--sha SHA]
|
||||
show ID [OWNER/REPOSITORY]
|
||||
logs ID [OWNER/REPOSITORY]";
|
||||
|
||||
@@ -59,33 +64,24 @@ pub async fn run(
|
||||
.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]
|
||||
[domain, group, action, arguments @ ..]
|
||||
if domain == "action" && group == "workflow" && action == "dispatch" =>
|
||||
{
|
||||
let arguments = dispatch_arguments(arguments, selection)?;
|
||||
dispatch(
|
||||
client,
|
||||
&scope(selection, Some(repository))?,
|
||||
workflow,
|
||||
reference,
|
||||
&arguments.repository,
|
||||
&arguments.workflow,
|
||||
&arguments.reference,
|
||||
&arguments.inputs,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
[domain, group, action] if domain == "action" && group == "run" && action == "list" => {
|
||||
print_runs(&client.action_runs(&scope(selection, None)?).await?);
|
||||
}
|
||||
[domain, group, action, repository]
|
||||
[domain, group, action, arguments @ ..]
|
||||
if domain == "action" && group == "run" && action == "list" =>
|
||||
{
|
||||
print_runs(
|
||||
&client
|
||||
.action_runs(&scope(selection, Some(repository))?)
|
||||
.await?,
|
||||
);
|
||||
let (repository, query) = run_list_arguments(arguments, selection)?;
|
||||
print_runs(&client.action_runs(&repository, &query).await?.items);
|
||||
}
|
||||
[domain, group, action, id] if domain == "action" && group == "run" && action == "show" => {
|
||||
print_run(
|
||||
@@ -161,14 +157,96 @@ async fn dispatch(
|
||||
repository: &RepositoryScope,
|
||||
workflow: &str,
|
||||
reference: &str,
|
||||
inputs: &std::collections::BTreeMap<String, String>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
client
|
||||
.dispatch_action_workflow(repository, workflow, reference)
|
||||
.dispatch_action_workflow(repository, workflow, reference, inputs)
|
||||
.await?;
|
||||
println!("Dispatched workflow {workflow} on {reference}.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct DispatchArguments {
|
||||
repository: RepositoryScope,
|
||||
workflow: String,
|
||||
reference: String,
|
||||
inputs: std::collections::BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
fn dispatch_arguments(
|
||||
arguments: &[String],
|
||||
selection: &Selection,
|
||||
) -> Result<DispatchArguments, Box<dyn Error>> {
|
||||
let [workflow, reference, remaining @ ..] = arguments else {
|
||||
return Err("usage: gotcha action workflow dispatch WORKFLOW REF [OWNER/REPOSITORY] [--input KEY=VALUE]…".into());
|
||||
};
|
||||
let mut repository = None;
|
||||
let mut inputs = Vec::new();
|
||||
let mut index = 0;
|
||||
while index < remaining.len() {
|
||||
if remaining[index] == "--input" {
|
||||
index += 1;
|
||||
inputs.push(
|
||||
remaining
|
||||
.get(index)
|
||||
.ok_or("--input requires KEY=VALUE")?
|
||||
.clone(),
|
||||
);
|
||||
} else if remaining[index].starts_with('-') {
|
||||
return Err(format!("unknown dispatch option: {}", remaining[index]).into());
|
||||
} else if repository.replace(remaining[index].as_str()).is_some() {
|
||||
return Err("only one OWNER/REPOSITORY may be supplied".into());
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
Ok(DispatchArguments {
|
||||
repository: scope(selection, repository)?,
|
||||
workflow: workflow.clone(),
|
||||
reference: reference.clone(),
|
||||
inputs: parse_action_inputs(&inputs)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_list_arguments(
|
||||
arguments: &[String],
|
||||
selection: &Selection,
|
||||
) -> Result<(RepositoryScope, ActionRunQuery), Box<dyn Error>> {
|
||||
let mut repository = None;
|
||||
let mut query = ActionRunQuery::default();
|
||||
let mut index = 0;
|
||||
while index < arguments.len() {
|
||||
let argument = &arguments[index];
|
||||
if argument.starts_with('-') {
|
||||
index += 1;
|
||||
let value = arguments
|
||||
.get(index)
|
||||
.ok_or_else(|| format!("{argument} requires a value"))?;
|
||||
match argument.as_str() {
|
||||
"--page" => query.page = positive_number(value, "page")?,
|
||||
"--limit" => query.limit = positive_number(value, "limit")?,
|
||||
"--status" => query.status = Some(value.clone()),
|
||||
"--event" => query.event = Some(value.clone()),
|
||||
"--branch" => query.branch = Some(value.clone()),
|
||||
"--actor" => query.actor = Some(value.clone()),
|
||||
"--sha" => query.head_sha = Some(value.clone()),
|
||||
_ => return Err(format!("unknown run-list option: {argument}").into()),
|
||||
}
|
||||
} else if repository.replace(argument.as_str()).is_some() {
|
||||
return Err("only one OWNER/REPOSITORY may be supplied".into());
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
Ok((scope(selection, repository)?, query))
|
||||
}
|
||||
|
||||
fn positive_number(value: &str, name: &str) -> Result<i32, Box<dyn Error>> {
|
||||
value
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
.filter(|value| *value > 0)
|
||||
.ok_or_else(|| format!("{name} must be a positive integer").into())
|
||||
}
|
||||
|
||||
fn print_workflows(workflows: &[ActionWorkflow]) {
|
||||
print_table(
|
||||
&[("ID", 40), ("STATE", 12), ("NAME", 50), ("PATH", 70)],
|
||||
@@ -299,4 +377,39 @@ mod tests {
|
||||
.contains("logs ID")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_dispatch_inputs_and_run_filters() {
|
||||
let selection = Selection {
|
||||
name: Some("gitea".into()),
|
||||
url: "https://gitea.example.com".into(),
|
||||
token: Some("secret".into()),
|
||||
provider: gotcha_gitea::Provider::Gitea,
|
||||
repository: Some(RepositoryScope::parse("hugo/Gotcha").unwrap()),
|
||||
};
|
||||
let dispatch = dispatch_arguments(
|
||||
&[
|
||||
"release.yml".into(),
|
||||
"main".into(),
|
||||
"--input".into(),
|
||||
"channel=stable".into(),
|
||||
],
|
||||
&selection,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(dispatch.inputs["channel"], "stable");
|
||||
|
||||
let (_, query) = run_list_arguments(
|
||||
&[
|
||||
"--page".into(),
|
||||
"2".into(),
|
||||
"--status".into(),
|
||||
"in_progress".into(),
|
||||
],
|
||||
&selection,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(query.page, 2);
|
||||
assert_eq!(query.status.as_deref(), Some("in_progress"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user