mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat(task): list tasks, get progress and set stage via CLI (#38471)
This commit is contained in:
75
cli/Cargo.lock
generated
75
cli/Cargo.lock
generated
@@ -93,9 +93,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.99"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
@@ -275,9 +275,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.47"
|
||||
version = "4.5.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
|
||||
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -285,9 +285,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.47"
|
||||
version = "4.5.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
|
||||
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -951,7 +951,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"hyper 1.7.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.31",
|
||||
"rustls 0.23.32",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.3",
|
||||
@@ -1228,9 +1228,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.80"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -1244,9 +1244,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
version = "0.2.176"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
@@ -1532,6 +1532,7 @@ name = "posthog-cli"
|
||||
version = "0.4.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm 0.28.1",
|
||||
"dirs",
|
||||
@@ -1622,7 +1623,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.31",
|
||||
"rustls 0.23.32",
|
||||
"socket2 0.6.0",
|
||||
"thiserror 2.0.16",
|
||||
"tokio",
|
||||
@@ -1642,7 +1643,7 @@ dependencies = [
|
||||
"rand",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.31",
|
||||
"rustls 0.23.32",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.16",
|
||||
@@ -1869,7 +1870,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.31",
|
||||
"rustls 0.23.32",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1953,9 +1954,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.31"
|
||||
version = "0.23.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
|
||||
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
@@ -2050,9 +2051,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.225"
|
||||
version = "1.0.226"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d"
|
||||
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
@@ -2060,18 +2061,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.225"
|
||||
version = "1.0.226"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383"
|
||||
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.225"
|
||||
version = "1.0.226"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516"
|
||||
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2490,7 +2491,7 @@ version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd"
|
||||
dependencies = [
|
||||
"rustls 0.23.31",
|
||||
"rustls 0.23.32",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -2806,9 +2807,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.103"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -2819,9 +2820,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.103"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -2833,9 +2834,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.53"
|
||||
version = "0.4.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
|
||||
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -2846,9 +2847,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.103"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -2856,9 +2857,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.103"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2869,18 +2870,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.103"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.80"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
|
||||
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
||||
@@ -22,6 +22,7 @@ path = "src/main.rs"
|
||||
clap = { version = "4.5.31", features = ["derive"] }
|
||||
dirs = "6.0.0"
|
||||
inquire = "0.7.5"
|
||||
chrono = "0.4.42"
|
||||
posthog-symbol-data = "0.2.0"
|
||||
walkdir = "2.5.0"
|
||||
globset = "0.4"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
pub mod login;
|
||||
pub mod query;
|
||||
pub mod sourcemap;
|
||||
pub mod tasks;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use query::QueryCommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::CapturedError;
|
||||
use crate::{commands::tasks::TaskCommand, error::CapturedError, utils::client::SKIP_SSL};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
@@ -25,6 +26,12 @@ pub enum Commands {
|
||||
/// environment variables `POSTHOG_CLI_TOKEN` and `POSTHOG_CLI_ENV_ID`
|
||||
Login,
|
||||
|
||||
/// Experimental commands, not quite ready for prime time
|
||||
Exp {
|
||||
#[command(subcommand)]
|
||||
cmd: ExpCommand,
|
||||
},
|
||||
|
||||
/// Run a SQL query against any data you have in posthog. This is mostly for fun, and subject to change
|
||||
Query {
|
||||
#[command(subcommand)]
|
||||
@@ -94,6 +101,20 @@ pub enum SourcemapCommand {
|
||||
Process(UploadArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ExpCommand {
|
||||
/// Manage tasks - list, create, update, delete etc
|
||||
Task {
|
||||
#[command(subcommand)]
|
||||
cmd: TaskCommand,
|
||||
/// Whether to skip SSL verification when talking to the posthog API - only use when using self-signed certificates for
|
||||
/// self-deployed instances
|
||||
// TODO - it seems likely we won't support tasks for self hosted, but I'm putting this here in case we do
|
||||
#[arg(long, default_value = "false")]
|
||||
skip_ssl_verification: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub fn run() -> Result<(), CapturedError> {
|
||||
let command = Cli::parse();
|
||||
@@ -115,6 +136,15 @@ impl Cli {
|
||||
}
|
||||
},
|
||||
Commands::Query { cmd } => query::query_command(command.host, cmd)?,
|
||||
Commands::Exp { cmd } => match cmd {
|
||||
ExpCommand::Task {
|
||||
cmd,
|
||||
skip_ssl_verification,
|
||||
} => {
|
||||
*SKIP_SSL.lock().unwrap() = *skip_ssl_verification;
|
||||
cmd.run()?;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -12,6 +12,7 @@ use tracing::{info, warn};
|
||||
|
||||
use crate::commands::UploadArgs;
|
||||
use crate::utils::auth::load_token;
|
||||
use crate::utils::client::{get_client, SKIP_SSL};
|
||||
use crate::utils::posthog::capture_command_invoked;
|
||||
use crate::utils::release::{create_release, CreateReleaseResponse};
|
||||
use crate::utils::sourcemaps::{read_pairs, ChunkUpload, SourcePair};
|
||||
@@ -57,6 +58,8 @@ pub fn upload(host: Option<String>, args: UploadArgs) -> Result<()> {
|
||||
batch_size,
|
||||
} = args;
|
||||
|
||||
*SKIP_SSL.lock().unwrap() = skip_ssl_verification;
|
||||
|
||||
let token = load_token().context("While starting upload command")?;
|
||||
let host = token.get_host(host.as_deref());
|
||||
|
||||
@@ -82,7 +85,6 @@ pub fn upload(host: Option<String>, args: UploadArgs) -> Result<()> {
|
||||
Some(content_hash(uploads.iter().map(|upload| &upload.data))),
|
||||
project,
|
||||
version,
|
||||
skip_ssl_verification,
|
||||
)
|
||||
.context("While creating release")?;
|
||||
|
||||
@@ -94,13 +96,7 @@ pub fn upload(host: Option<String>, args: UploadArgs) -> Result<()> {
|
||||
// We could relax this, such that we instead replace the existing release with the new one,
|
||||
// or we could even just allow adding new chunks to an existing release, but for now I'm
|
||||
// leaving it like this... Reviewers, lets chat about the right approach here
|
||||
upload_chunks(
|
||||
&base_url,
|
||||
&token.token,
|
||||
batch_upload,
|
||||
release.as_ref(),
|
||||
skip_ssl_verification,
|
||||
)?;
|
||||
upload_chunks(&base_url, &token.token, batch_upload, release.as_ref())?;
|
||||
}
|
||||
|
||||
if delete_after {
|
||||
@@ -135,11 +131,8 @@ fn upload_chunks(
|
||||
token: &str,
|
||||
uploads: Vec<ChunkUpload>,
|
||||
release: Option<&CreateReleaseResponse>,
|
||||
skip_ssl_verification: bool,
|
||||
) -> Result<()> {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.danger_accept_invalid_certs(skip_ssl_verification)
|
||||
.build()?;
|
||||
let client = get_client()?;
|
||||
|
||||
let release_id = release.map(|r| r.id.to_string());
|
||||
let chunk_ids = uploads
|
||||
|
||||
202
cli/src/commands/tasks/list.rs
Normal file
202
cli/src/commands/tasks/list.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::blocking::Client;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::{TaskListResponse, TaskWorkflow, WorkflowStage};
|
||||
use crate::{
|
||||
commands::tasks::{
|
||||
utils::{fetch_stages, fetch_tasks, fetch_workflows},
|
||||
Task,
|
||||
},
|
||||
utils::auth::Token,
|
||||
};
|
||||
|
||||
const BUFFER_SIZE: usize = 50;
|
||||
|
||||
pub struct TaskIterator {
|
||||
client: Client,
|
||||
token: Token,
|
||||
buffer: VecDeque<Task>,
|
||||
next_url: Option<String>,
|
||||
}
|
||||
|
||||
impl TaskIterator {
|
||||
pub fn new(client: Client, host: String, token: Token, offset: Option<usize>) -> Result<Self> {
|
||||
let initial_url = format!("{}/api/environments/{}/tasks/", host, token.env_id);
|
||||
let mut params = vec![];
|
||||
params.push(("limit", BUFFER_SIZE.to_string()));
|
||||
if let Some(offset) = offset {
|
||||
params.push(("offset", offset.to_string()));
|
||||
}
|
||||
|
||||
let response = client
|
||||
.get(&initial_url)
|
||||
.query(¶ms)
|
||||
.header("Authorization", format!("Bearer {}", token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.unwrap_or_else(|_| "No response body".to_string());
|
||||
anyhow::bail!("Failed to fetch tasks: {} - {}", status, body);
|
||||
}
|
||||
|
||||
let task_response: TaskListResponse = response
|
||||
.json()
|
||||
.context("Failed to parse task list response")?;
|
||||
|
||||
let mut buffer = VecDeque::new();
|
||||
buffer.extend(task_response.results);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
token,
|
||||
buffer,
|
||||
next_url: task_response.next,
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_next_batch(&mut self) -> Result<bool> {
|
||||
let url = if let Some(next_url) = &self.next_url {
|
||||
next_url.clone()
|
||||
} else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.unwrap_or_else(|_| "No response body".to_string());
|
||||
anyhow::bail!("Failed to fetch tasks: {} - {}", status, body);
|
||||
}
|
||||
|
||||
let task_response: TaskListResponse = response
|
||||
.json()
|
||||
.context("Failed to parse task list response")?;
|
||||
|
||||
self.buffer.extend(task_response.results);
|
||||
self.next_url = task_response.next;
|
||||
|
||||
Ok(!self.buffer.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TaskIterator {
|
||||
type Item = Result<Task>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.buffer.is_empty() {
|
||||
match self.fetch_next_batch() {
|
||||
Ok(has_data) => {
|
||||
if !has_data {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(e) => return Some(Err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
self.buffer.pop_front().map(Ok)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_task(task: &Task, workflows: &[TaskWorkflow], stages: &[WorkflowStage]) {
|
||||
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
println!("ID: {}", task.id);
|
||||
println!("Title: {}", task.title);
|
||||
|
||||
if let Some(desc) = &task.description {
|
||||
if !desc.is_empty() {
|
||||
println!("Description: {desc}");
|
||||
}
|
||||
}
|
||||
|
||||
println!("Origin Product: {}", task.origin_product);
|
||||
println!("Position: {}", task.position);
|
||||
|
||||
if let Some(workflow_id) = &task.workflow {
|
||||
if let Some(workflow) = workflows.iter().find(|w| &w.id == workflow_id) {
|
||||
println!(
|
||||
"Workflow: {}{}",
|
||||
workflow.name,
|
||||
if workflow.is_default {
|
||||
" (default)"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
} else {
|
||||
println!("Workflow: {workflow_id} (unknown)");
|
||||
}
|
||||
} else {
|
||||
println!("Workflow: None");
|
||||
}
|
||||
|
||||
if let Some(stage_id) = &task.current_stage {
|
||||
if let Some(stage) = stages.iter().find(|s| &s.id == stage_id) {
|
||||
println!("Stage: {} ({})", stage.name, stage.key);
|
||||
} else {
|
||||
println!("Stage: {stage_id} (unknown)");
|
||||
}
|
||||
} else {
|
||||
println!("Stage: None");
|
||||
}
|
||||
|
||||
if let Some(primary_repo) = &task.primary_repository {
|
||||
if let Some(org) = primary_repo.get("organization").and_then(|v| v.as_str()) {
|
||||
if let Some(repo) = primary_repo.get("repository").and_then(|v| v.as_str()) {
|
||||
println!("Repository: {org}/{repo}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(branch) = &task.github_branch {
|
||||
println!("GitHub Branch: {branch}");
|
||||
}
|
||||
|
||||
if let Some(pr_url) = &task.github_pr_url {
|
||||
println!("GitHub PR: {pr_url}");
|
||||
}
|
||||
|
||||
println!("Created: {}", task.created_at.format("%Y-%m-%d %H:%M UTC"));
|
||||
println!("Updated: {}", task.updated_at.format("%Y-%m-%d %H:%M UTC"));
|
||||
}
|
||||
|
||||
pub fn list_tasks(limit: Option<&usize>, offset: Option<&usize>) -> Result<()> {
|
||||
let token = crate::utils::auth::load_token().context("Failed to load authentication token")?;
|
||||
let host = token.get_host(None);
|
||||
let client = Client::new();
|
||||
|
||||
let workflows: Result<Vec<TaskWorkflow>> =
|
||||
fetch_workflows(client.clone(), host.clone(), token.clone())?.collect();
|
||||
let workflows = workflows?;
|
||||
|
||||
let stages: Result<Vec<WorkflowStage>> =
|
||||
fetch_stages(client.clone(), host.clone(), token.clone())?.collect();
|
||||
let stages = stages?;
|
||||
|
||||
let tasks = fetch_tasks(client, host, token, offset.cloned())?;
|
||||
|
||||
let task_iter: Box<dyn Iterator<Item = Result<Task>>> = if let Some(limit) = limit {
|
||||
Box::new(tasks.take(*limit))
|
||||
} else {
|
||||
Box::new(tasks)
|
||||
};
|
||||
|
||||
for task in task_iter {
|
||||
print_task(&task?, &workflows, &stages);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
141
cli/src/commands/tasks/mod.rs
Normal file
141
cli/src/commands/tasks/mod.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
mod list;
|
||||
mod progress;
|
||||
mod update_stage;
|
||||
mod utils;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Subcommand;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::list::list_tasks;
|
||||
use self::progress::show_progress;
|
||||
use self::update_stage::update_stage;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub origin_product: String,
|
||||
pub position: i32,
|
||||
pub workflow: Option<Uuid>,
|
||||
pub current_stage: Option<Uuid>,
|
||||
pub github_integration: Option<i64>,
|
||||
pub repository_config: Option<Value>,
|
||||
pub repository_list: Option<Vec<Value>>,
|
||||
pub primary_repository: Option<Value>,
|
||||
pub github_branch: Option<String>,
|
||||
pub github_pr_url: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WorkflowStage {
|
||||
pub id: Uuid,
|
||||
pub workflow: Uuid,
|
||||
pub name: String,
|
||||
pub key: String,
|
||||
pub position: i32,
|
||||
pub color: String,
|
||||
pub agent: Option<Uuid>,
|
||||
pub agent_name: Option<String>,
|
||||
pub is_manual_only: bool,
|
||||
pub is_archived: bool,
|
||||
pub fallback_stage: Option<Uuid>,
|
||||
pub task_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AgentDefinition {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub agent_type: String,
|
||||
pub description: Option<String>,
|
||||
pub config: Value, // JSON object
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TaskWorkflow {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub color: String,
|
||||
pub is_default: bool,
|
||||
pub is_active: bool,
|
||||
pub version: i32,
|
||||
pub stages: Vec<WorkflowStage>,
|
||||
pub task_count: Option<i32>,
|
||||
pub can_delete: Option<CanDeleteResponse>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CanDeleteResponse {
|
||||
pub can_delete: bool,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TaskListResponse {
|
||||
pub results: Vec<Task>,
|
||||
pub count: usize,
|
||||
pub next: Option<String>,
|
||||
pub previous: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RepositoryConfig {
|
||||
pub integration_id: Option<i64>,
|
||||
pub organization: String,
|
||||
pub repository: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WorkflowConfiguration {
|
||||
pub workflow: TaskWorkflow,
|
||||
pub stages: Vec<WorkflowStage>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum TaskCommand {
|
||||
/// List all tasks
|
||||
List {
|
||||
/// Maximum number of tasks to display
|
||||
#[arg(long)]
|
||||
limit: Option<usize>,
|
||||
|
||||
/// Page offset for pagination
|
||||
#[arg(long)]
|
||||
offset: Option<usize>,
|
||||
},
|
||||
|
||||
/// View task progress
|
||||
Progress {
|
||||
/// Task ID (will prompt for selection if not provided)
|
||||
task_id: Option<Uuid>,
|
||||
},
|
||||
|
||||
/// Update task stage
|
||||
UpdateStage {
|
||||
/// Task ID (will prompt for selection if not provided)
|
||||
task_id: Option<Uuid>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TaskCommand {
|
||||
pub fn run(&self) -> Result<()> {
|
||||
match self {
|
||||
TaskCommand::List { limit, offset } => list_tasks(limit.as_ref(), offset.as_ref()),
|
||||
TaskCommand::Progress { task_id } => show_progress(task_id.as_ref()),
|
||||
TaskCommand::UpdateStage { task_id } => update_stage(task_id.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
178
cli/src/commands/tasks/progress.rs
Normal file
178
cli/src/commands/tasks/progress.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::commands::tasks::utils::select_task;
|
||||
use crate::utils::auth::load_token;
|
||||
use crate::utils::client::get_client;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct TaskProgressResponse {
|
||||
has_progress: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
id: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
status: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
current_step: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
completed_steps: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
total_steps: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
progress_percentage: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
output_log: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error_message: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
completed_at: Option<DateTime<Utc>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
workflow_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
workflow_run_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
pub fn show_progress(task_id: Option<&Uuid>) -> Result<()> {
|
||||
// Get the task ID either from the argument or through interactive selection
|
||||
let task_id = match task_id {
|
||||
Some(id) => *id,
|
||||
None => select_task("Select a task to view progress:")?.id,
|
||||
};
|
||||
|
||||
// Fetch and display progress
|
||||
let progress = fetch_progress(&task_id)?;
|
||||
print_progress(&task_id, &progress);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_progress(task_id: &Uuid) -> Result<TaskProgressResponse> {
|
||||
let token = load_token().context("Failed to load authentication token")?;
|
||||
let host = token.get_host(None);
|
||||
let client = get_client()?;
|
||||
|
||||
let url = format!(
|
||||
"{}/api/environments/{}/tasks/{}/progress/",
|
||||
host, token.env_id, task_id
|
||||
);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.unwrap_or_else(|_| "No response body".to_string());
|
||||
anyhow::bail!("Failed to fetch progress: {} - {}", status, body);
|
||||
}
|
||||
|
||||
let progress: TaskProgressResponse = response
|
||||
.json()
|
||||
.context("Failed to parse progress response")?;
|
||||
|
||||
Ok(progress)
|
||||
}
|
||||
|
||||
fn print_progress(task_id: &Uuid, progress: &TaskProgressResponse) {
|
||||
println!("\nProgress for Task {task_id}:\n");
|
||||
|
||||
if !progress.has_progress {
|
||||
println!(
|
||||
"{}",
|
||||
progress
|
||||
.message
|
||||
.as_deref()
|
||||
.unwrap_or("No execution progress found for this task")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(status) = &progress.status {
|
||||
println!("Status: {status}");
|
||||
}
|
||||
|
||||
if let Some(percentage) = progress.progress_percentage {
|
||||
let filled = (percentage / 2.0) as usize;
|
||||
let empty = 50 - filled;
|
||||
let bar = format!(
|
||||
"[{}{}] {:.1}%",
|
||||
"█".repeat(filled),
|
||||
"░".repeat(empty),
|
||||
percentage
|
||||
);
|
||||
println!("Progress: {bar}");
|
||||
}
|
||||
|
||||
if let Some(current_step) = &progress.current_step {
|
||||
if !current_step.is_empty() {
|
||||
println!("Current Step: {current_step}");
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(completed), Some(total)) = (progress.completed_steps, progress.total_steps) {
|
||||
println!("Steps: {completed} / {total}");
|
||||
}
|
||||
|
||||
if let Some(workflow_id) = &progress.workflow_id {
|
||||
if !workflow_id.is_empty() {
|
||||
println!("\nWorkflow ID: {workflow_id}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(workflow_run_id) = &progress.workflow_run_id {
|
||||
if !workflow_run_id.is_empty() {
|
||||
println!("Workflow Run ID: {workflow_run_id}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(output_log) = &progress.output_log {
|
||||
if !output_log.is_empty() {
|
||||
println!("\nOutput:");
|
||||
println!("{}", "─".repeat(60));
|
||||
for line in output_log.lines() {
|
||||
println!("{line}");
|
||||
}
|
||||
println!("{}", "─".repeat(60));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(error_message) = &progress.error_message {
|
||||
if !error_message.is_empty() {
|
||||
println!("\nError:");
|
||||
println!("{}", "─".repeat(60));
|
||||
for line in error_message.lines() {
|
||||
println!("{line}");
|
||||
}
|
||||
println!("{}", "─".repeat(60));
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nTimestamps:");
|
||||
if let Some(created_at) = progress.created_at {
|
||||
println!(" Started: {}", created_at.format("%Y-%m-%d %H:%M:%S UTC"));
|
||||
}
|
||||
if let Some(updated_at) = progress.updated_at {
|
||||
println!(
|
||||
" Last Updated: {}",
|
||||
updated_at.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
}
|
||||
if let Some(completed_at) = progress.completed_at {
|
||||
println!(
|
||||
" Completed: {}",
|
||||
completed_at.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
}
|
||||
}
|
||||
183
cli/src/commands/tasks/update_stage.rs
Normal file
183
cli/src/commands/tasks/update_stage.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use anyhow::{Context, Result};
|
||||
use inquire::Select;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{Task, TaskWorkflow, WorkflowStage};
|
||||
use crate::commands::tasks::utils::select_task;
|
||||
use crate::utils::auth::load_token;
|
||||
use crate::utils::client::get_client;
|
||||
|
||||
struct StageChoice(WorkflowStage);
|
||||
|
||||
impl std::fmt::Display for StageChoice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} ({})", self.0.name, self.0.key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct UpdateStageRequest {
|
||||
current_stage: Uuid,
|
||||
}
|
||||
|
||||
pub fn update_stage(task_id: Option<&Uuid>) -> Result<()> {
|
||||
let token = load_token().context("Failed to load authentication token")?;
|
||||
let host = token.get_host(None);
|
||||
let client = get_client()?;
|
||||
|
||||
let task = match task_id {
|
||||
Some(id) => fetch_task(&client, &host, &token, id)?,
|
||||
None => select_task("Select a task to update stage:")?,
|
||||
};
|
||||
|
||||
println!("\nTask: {}", task.title);
|
||||
|
||||
let workflow_id = match task.workflow {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
anyhow::bail!("This task is not associated with any workflow. Cannot update stage.");
|
||||
}
|
||||
};
|
||||
|
||||
let workflow = fetch_workflow(&client, &host, &token, &workflow_id)?;
|
||||
|
||||
if workflow.stages.is_empty() {
|
||||
anyhow::bail!("The workflow '{}' has no stages defined.", workflow.name);
|
||||
}
|
||||
|
||||
if let Some(current_stage_id) = &task.current_stage {
|
||||
if let Some(current_stage) = workflow.stages.iter().find(|s| &s.id == current_stage_id) {
|
||||
println!(
|
||||
"Current Stage: {} ({})",
|
||||
current_stage.name, current_stage.key
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!("Current Stage: None");
|
||||
}
|
||||
|
||||
let available_stages: Vec<StageChoice> = workflow
|
||||
.stages
|
||||
.into_iter()
|
||||
.filter(|s| !s.is_archived)
|
||||
.map(StageChoice)
|
||||
.collect();
|
||||
|
||||
if available_stages.is_empty() {
|
||||
anyhow::bail!("No active stages available in the workflow.");
|
||||
}
|
||||
|
||||
println!("\nAvailable stages:");
|
||||
|
||||
let selected_stage = Select::new("Select new stage:", available_stages)
|
||||
.prompt()
|
||||
.context("Failed to get stage selection")?;
|
||||
|
||||
update_task_stage(&client, &host, &token, &task.id, &selected_stage.0.id)?;
|
||||
|
||||
println!(
|
||||
"\n✓ Successfully updated task stage to: {} ({})",
|
||||
selected_stage.0.name, selected_stage.0.key
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_task(
|
||||
client: &Client,
|
||||
host: &str,
|
||||
token: &crate::utils::auth::Token,
|
||||
task_id: &Uuid,
|
||||
) -> Result<Task> {
|
||||
let url = format!(
|
||||
"{}/api/environments/{}/tasks/{}/",
|
||||
host, token.env_id, task_id
|
||||
);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.unwrap_or_else(|_| "No response body".to_string());
|
||||
anyhow::bail!("Failed to fetch task: {} - {}", status, body);
|
||||
}
|
||||
|
||||
let task: Task = response.json().context("Failed to parse task response")?;
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
fn fetch_workflow(
|
||||
client: &Client,
|
||||
host: &str,
|
||||
token: &crate::utils::auth::Token,
|
||||
workflow_id: &Uuid,
|
||||
) -> Result<TaskWorkflow> {
|
||||
let url = format!(
|
||||
"{}/api/environments/{}/task_workflows/{}/",
|
||||
host, token.env_id, workflow_id
|
||||
);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.unwrap_or_else(|_| "No response body".to_string());
|
||||
anyhow::bail!("Failed to fetch workflow: {} - {}", status, body);
|
||||
}
|
||||
|
||||
let workflow: TaskWorkflow = response
|
||||
.json()
|
||||
.context("Failed to parse workflow response")?;
|
||||
|
||||
Ok(workflow)
|
||||
}
|
||||
|
||||
fn update_task_stage(
|
||||
client: &Client,
|
||||
host: &str,
|
||||
token: &crate::utils::auth::Token,
|
||||
task_id: &Uuid,
|
||||
stage_id: &Uuid,
|
||||
) -> Result<()> {
|
||||
let url = format!(
|
||||
"{}/api/environments/{}/tasks/{}/update_stage/",
|
||||
host, token.env_id, task_id
|
||||
);
|
||||
|
||||
let request_body = UpdateStageRequest {
|
||||
current_stage: *stage_id,
|
||||
};
|
||||
|
||||
let response = client
|
||||
.patch(&url)
|
||||
.header("Authorization", format!("Bearer {}", token.token))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.unwrap_or_else(|_| "No response body".to_string());
|
||||
anyhow::bail!("Failed to update stage: {} - {}", status, body);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
281
cli/src/commands/tasks/utils.rs
Normal file
281
cli/src/commands/tasks/utils.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use anyhow::{Context, Result};
|
||||
use inquire::Select;
|
||||
use reqwest::blocking::Client;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::{Task, TaskWorkflow, WorkflowStage};
|
||||
use crate::commands::tasks::list::TaskIterator;
|
||||
use crate::utils::auth::Token;
|
||||
|
||||
const PAGE_SIZE: usize = 10;
|
||||
const BUFFER_SIZE: usize = 50;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum SelectionChoice {
|
||||
Task(Task),
|
||||
Next,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SelectionChoice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SelectionChoice::Task(task) => {
|
||||
write!(f, "{} - {}", task.title, task.origin_product)
|
||||
}
|
||||
SelectionChoice::Next => write!(f, "→ Load more tasks..."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_task(prompt: &str) -> Result<Task> {
|
||||
let token = crate::utils::auth::load_token().context("Failed to load authentication token")?;
|
||||
let host = token.get_host(None);
|
||||
let client = crate::utils::client::get_client()?;
|
||||
|
||||
let mut task_iter = fetch_tasks(client, host, token, None)?;
|
||||
|
||||
loop {
|
||||
let mut choices = Vec::new();
|
||||
|
||||
// Fetch up to PAGE_SIZE tasks
|
||||
for _ in 0..PAGE_SIZE {
|
||||
match task_iter.next() {
|
||||
Some(Ok(task)) => choices.push(SelectionChoice::Task(task)),
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
if choices.is_empty() {
|
||||
anyhow::bail!("No tasks found.");
|
||||
}
|
||||
|
||||
// If we got exactly PAGE_SIZE items, assume there might be more
|
||||
if choices.len() == PAGE_SIZE {
|
||||
choices.push(SelectionChoice::Next);
|
||||
}
|
||||
|
||||
let selection = Select::new(prompt, choices)
|
||||
.prompt()
|
||||
.context("Failed to get task selection")?;
|
||||
|
||||
match selection {
|
||||
SelectionChoice::Task(task) => return Ok(task),
|
||||
SelectionChoice::Next => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_tasks(
|
||||
client: Client,
|
||||
host: String,
|
||||
token: Token,
|
||||
offset: Option<usize>,
|
||||
) -> Result<TaskIterator> {
|
||||
TaskIterator::new(client, host, token, offset)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WorkflowListResponse {
|
||||
results: Vec<TaskWorkflow>,
|
||||
next: Option<String>,
|
||||
}
|
||||
|
||||
pub struct WorkflowIterator {
|
||||
client: Client,
|
||||
token: Token,
|
||||
buffer: VecDeque<TaskWorkflow>,
|
||||
next_url: Option<String>,
|
||||
}
|
||||
|
||||
impl WorkflowIterator {
|
||||
fn new(client: Client, host: String, token: Token) -> Result<Self> {
|
||||
let initial_url = format!(
|
||||
"{}/api/environments/{}/task_workflows/?limit={}",
|
||||
host, token.env_id, BUFFER_SIZE
|
||||
);
|
||||
|
||||
let response = client
|
||||
.get(&initial_url)
|
||||
.header("Authorization", format!("Bearer {}", token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
// Return empty iterator on error, don't fail
|
||||
return Ok(Self {
|
||||
client,
|
||||
token,
|
||||
buffer: VecDeque::new(),
|
||||
next_url: None,
|
||||
});
|
||||
}
|
||||
|
||||
let workflow_response: WorkflowListResponse = response
|
||||
.json()
|
||||
.context("Failed to parse workflow list response")?;
|
||||
|
||||
let mut buffer = VecDeque::new();
|
||||
buffer.extend(workflow_response.results);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
token,
|
||||
buffer,
|
||||
next_url: workflow_response.next,
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_next_batch(&mut self) -> Result<bool> {
|
||||
let url = if let Some(next_url) = &self.next_url {
|
||||
next_url.clone()
|
||||
} else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let workflow_response: WorkflowListResponse = response
|
||||
.json()
|
||||
.context("Failed to parse workflow list response")?;
|
||||
|
||||
self.buffer.extend(workflow_response.results);
|
||||
self.next_url = workflow_response.next;
|
||||
|
||||
Ok(!self.buffer.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for WorkflowIterator {
|
||||
type Item = Result<TaskWorkflow>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.buffer.is_empty() {
|
||||
match self.fetch_next_batch() {
|
||||
Ok(has_data) => {
|
||||
if !has_data {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(e) => return Some(Err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
self.buffer.pop_front().map(Ok)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_workflows(client: Client, host: String, token: Token) -> Result<WorkflowIterator> {
|
||||
WorkflowIterator::new(client, host, token)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct StageListResponse {
|
||||
results: Vec<WorkflowStage>,
|
||||
next: Option<String>,
|
||||
}
|
||||
|
||||
pub struct StageIterator {
|
||||
client: Client,
|
||||
token: Token,
|
||||
buffer: VecDeque<WorkflowStage>,
|
||||
next_url: Option<String>,
|
||||
}
|
||||
|
||||
impl StageIterator {
|
||||
fn new(client: Client, host: String, token: Token) -> Result<Self> {
|
||||
let initial_url = format!(
|
||||
"{}/api/environments/{}/workflow_stages/?limit={}",
|
||||
host, token.env_id, BUFFER_SIZE
|
||||
);
|
||||
|
||||
let response = client
|
||||
.get(&initial_url)
|
||||
.header("Authorization", format!("Bearer {}", token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Ok(Self {
|
||||
client,
|
||||
token,
|
||||
buffer: VecDeque::new(),
|
||||
next_url: None,
|
||||
});
|
||||
}
|
||||
|
||||
let stage_response: StageListResponse = response
|
||||
.json()
|
||||
.context("Failed to parse stage list response")?;
|
||||
|
||||
let mut buffer = VecDeque::new();
|
||||
buffer.extend(stage_response.results);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
token,
|
||||
buffer,
|
||||
next_url: stage_response.next,
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_next_batch(&mut self) -> Result<bool> {
|
||||
let url = if let Some(next_url) = &self.next_url {
|
||||
next_url.clone()
|
||||
} else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.token.token))
|
||||
.send()
|
||||
.context("Failed to send request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let stage_response: StageListResponse = response
|
||||
.json()
|
||||
.context("Failed to parse stage list response")?;
|
||||
|
||||
self.buffer.extend(stage_response.results);
|
||||
self.next_url = stage_response.next;
|
||||
|
||||
Ok(!self.buffer.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for StageIterator {
|
||||
type Item = Result<WorkflowStage>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.buffer.is_empty() {
|
||||
match self.fetch_next_batch() {
|
||||
Ok(has_data) => {
|
||||
if !has_data {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(e) => return Some(Err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
self.buffer.pop_front().map(Ok)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_stages(client: Client, host: String, token: Token) -> Result<StageIterator> {
|
||||
StageIterator::new(client, host, token)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub struct QueryTui {
|
||||
state_dirty: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum LowerPanelState {
|
||||
TableState(TableState),
|
||||
DebugState(TextArea<'static>),
|
||||
|
||||
14
cli/src/utils/client.rs
Normal file
14
cli/src/utils/client.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::Result;
|
||||
use reqwest::blocking::Client;
|
||||
|
||||
// Olly can have a little global state, as a treat. I could make this a OnceCell, to assert it's written to exactly once, and that'd
|
||||
// probably make it more correct, or at least more difficult to misuse. Alas, war mode.
|
||||
pub static SKIP_SSL: Mutex<bool> = Mutex::new(false);
|
||||
|
||||
pub fn get_client() -> Result<Client> {
|
||||
Ok(reqwest::blocking::Client::builder()
|
||||
.danger_accept_invalid_certs(*SKIP_SSL.lock().unwrap())
|
||||
.build()?)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod client;
|
||||
pub mod constant;
|
||||
pub mod git;
|
||||
pub mod homedir;
|
||||
|
||||
@@ -6,6 +6,8 @@ use serde_json::Value;
|
||||
use tracing::{info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::utils::client::get_client;
|
||||
|
||||
use super::{auth::Token, git::get_git_info};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -30,7 +32,6 @@ pub fn create_release(
|
||||
hash_id: Option<String>,
|
||||
project: Option<String>,
|
||||
version: Option<String>,
|
||||
skip_ssl_verification: bool,
|
||||
) -> Result<Option<CreateReleaseResponse>> {
|
||||
let git_info = get_git_info(dir)?;
|
||||
|
||||
@@ -64,9 +65,7 @@ pub fn create_release(
|
||||
host, token.env_id
|
||||
);
|
||||
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.danger_accept_invalid_certs(skip_ssl_verification)
|
||||
.build()?;
|
||||
let client = get_client()?;
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
|
||||
Reference in New Issue
Block a user