mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
fabro(01KKS6FZ8NX3ETDF525W0HRVA8): implement (success)
Fabro-Run: 01KKS6FZ8NX3ETDF525W0HRVA8
Fabro-Completed: 5
Fabro-Checkpoint: 9ea9c6899e
⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
parent
1bda02fb50
commit
dd93fef91c
2 changed files with 143 additions and 27 deletions
|
|
@ -685,7 +685,8 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
install::run_install().await?;
|
||||
}
|
||||
Command::Ps(args) => {
|
||||
fabro_workflows::cli::runs::list_command(&args)?;
|
||||
let styles = fabro_util::terminal::Styles::detect_stdout();
|
||||
fabro_workflows::cli::runs::list_command(&args, &styles)?;
|
||||
}
|
||||
Command::Pr { command } => {
|
||||
let cli_config = cli_config::load_cli_config(None)?;
|
||||
|
|
@ -750,4 +751,4 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
.await;
|
||||
|
||||
(command_name, result)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Args;
|
||||
use fabro_util::terminal::Styles;
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
|
|
@ -70,6 +72,14 @@ pub struct RunsListArgs {
|
|||
/// Output as JSON
|
||||
#[arg(long)]
|
||||
pub json: bool,
|
||||
|
||||
/// Maximum number of runs to display (default: 10)
|
||||
#[arg(long, default_value = "10")]
|
||||
pub limit: usize,
|
||||
|
||||
/// Show all runs (overrides --limit)
|
||||
#[arg(long)]
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -96,6 +106,10 @@ pub struct RunInfo {
|
|||
pub status: RunStatus,
|
||||
pub start_time: String,
|
||||
pub labels: HashMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub duration_ms: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub total_cost: Option<f64>,
|
||||
#[serde(skip)]
|
||||
pub start_time_dt: Option<DateTime<Utc>>,
|
||||
#[serde(skip)]
|
||||
|
|
@ -137,18 +151,20 @@ pub fn scan_runs(base: &Path) -> Result<Vec<RunInfo>> {
|
|||
let start_time = start_time_dt.to_rfc3339();
|
||||
let labels = manifest.labels;
|
||||
|
||||
let (status, end_time) = read_status(&path);
|
||||
let si = read_status(&path);
|
||||
|
||||
runs.push(RunInfo {
|
||||
run_id,
|
||||
dir_name,
|
||||
workflow_name,
|
||||
workflow_slug,
|
||||
status,
|
||||
status: si.status,
|
||||
start_time,
|
||||
labels,
|
||||
duration_ms: si.duration_ms,
|
||||
total_cost: si.total_cost,
|
||||
start_time_dt: Some(start_time_dt),
|
||||
end_time,
|
||||
end_time: si.end_time,
|
||||
path,
|
||||
is_orphan: false,
|
||||
});
|
||||
|
|
@ -169,6 +185,8 @@ pub fn scan_runs(base: &Path) -> Result<Vec<RunInfo>> {
|
|||
status: RunStatus::Unknown,
|
||||
start_time: mtime,
|
||||
labels: HashMap::new(),
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
start_time_dt: mtime_dt,
|
||||
end_time: None,
|
||||
path,
|
||||
|
|
@ -182,17 +200,36 @@ pub fn scan_runs(base: &Path) -> Result<Vec<RunInfo>> {
|
|||
Ok(runs)
|
||||
}
|
||||
|
||||
fn read_status(run_dir: &Path) -> (RunStatus, Option<DateTime<Utc>>) {
|
||||
struct StatusInfo {
|
||||
status: RunStatus,
|
||||
end_time: Option<DateTime<Utc>>,
|
||||
duration_ms: Option<u64>,
|
||||
total_cost: Option<f64>,
|
||||
}
|
||||
|
||||
fn read_status(run_dir: &Path) -> StatusInfo {
|
||||
if let Ok(conclusion) = crate::conclusion::Conclusion::load(&run_dir.join("conclusion.json")) {
|
||||
return (
|
||||
RunStatus::Concluded(conclusion.status),
|
||||
Some(conclusion.timestamp),
|
||||
);
|
||||
return StatusInfo {
|
||||
status: RunStatus::Concluded(conclusion.status),
|
||||
end_time: Some(conclusion.timestamp),
|
||||
duration_ms: Some(conclusion.duration_ms),
|
||||
total_cost: conclusion.total_cost,
|
||||
};
|
||||
}
|
||||
if run_dir.join("run.pid").exists() {
|
||||
return (RunStatus::Running, None);
|
||||
return StatusInfo {
|
||||
status: RunStatus::Running,
|
||||
end_time: None,
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
};
|
||||
}
|
||||
StatusInfo {
|
||||
status: RunStatus::Unknown,
|
||||
end_time: None,
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
}
|
||||
(RunStatus::Unknown, None)
|
||||
}
|
||||
|
||||
/// Filter runs by criteria. Orphans are excluded unless `include_orphans` is true.
|
||||
|
|
@ -345,7 +382,44 @@ fn collapse_separators(s: &str) -> String {
|
|||
s.chars().filter(|c| *c != '-' && *c != '_').collect()
|
||||
}
|
||||
|
||||
pub fn list_command(args: &RunsListArgs) -> Result<()> {
|
||||
/// Format a `DateTime<Utc>` as a human-friendly relative string like "2m ago", "3h ago", "5d ago".
|
||||
fn format_relative_time(dt: &DateTime<Utc>) -> String {
|
||||
let now = Utc::now();
|
||||
let dur = now.signed_duration_since(*dt);
|
||||
let secs = dur.num_seconds();
|
||||
if secs < 0 {
|
||||
return "just now".to_string();
|
||||
}
|
||||
if secs < 60 {
|
||||
return "just now".to_string();
|
||||
}
|
||||
let mins = dur.num_minutes();
|
||||
if mins < 60 {
|
||||
return format!("{mins}m ago");
|
||||
}
|
||||
let hours = dur.num_hours();
|
||||
if hours < 24 {
|
||||
return format!("{hours}h ago");
|
||||
}
|
||||
let days = dur.num_days();
|
||||
format!("{days}d ago")
|
||||
}
|
||||
|
||||
fn style_status<'a>(status: &'a RunStatus, styles: &'a Styles) -> String {
|
||||
let text = status.to_string();
|
||||
match status {
|
||||
RunStatus::Concluded(StageStatus::Success | StageStatus::PartialSuccess) => {
|
||||
format!("{}", styles.bold_green.apply_to(&text))
|
||||
}
|
||||
RunStatus::Concluded(StageStatus::Fail) => {
|
||||
format!("{}", styles.bold_red.apply_to(&text))
|
||||
}
|
||||
RunStatus::Running => format!("{}", styles.bold_cyan.apply_to(&text)),
|
||||
_ => format!("{}", styles.dim.apply_to(&text)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
|
||||
let base = default_runs_base();
|
||||
let runs = scan_runs(&base)?;
|
||||
let label_filters = parse_label_filters(&args.filter.label);
|
||||
|
|
@ -367,15 +441,19 @@ pub fn list_command(args: &RunsListArgs) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let total = filtered.len();
|
||||
let limit = if args.all { total } else { args.limit };
|
||||
let display_runs: Vec<_> = filtered.iter().take(limit).collect();
|
||||
|
||||
// Print table header
|
||||
let header = format!(
|
||||
"{:<30} {:<25} {:<10} {:<25} LABELS",
|
||||
"RUN ID", "WORKFLOW", "STATUS", "STARTED"
|
||||
"{:<30} {:<25} {:<17} {:<10} {:<10} {:<10} LABELS",
|
||||
"RUN ID", "WORKFLOW", "STATUS", "STARTED", "DURATION", "COST"
|
||||
);
|
||||
println!("{header}");
|
||||
println!("{}", "-".repeat(100));
|
||||
println!("{}", styles.bold.apply_to(&header));
|
||||
println!("{}", styles.dim.apply_to("-".repeat(110)));
|
||||
|
||||
for run in &filtered {
|
||||
for run in &display_runs {
|
||||
let labels_str = run
|
||||
.labels
|
||||
.iter()
|
||||
|
|
@ -387,17 +465,40 @@ pub fn list_command(args: &RunsListArgs) -> Result<()> {
|
|||
} else {
|
||||
run.run_id.clone()
|
||||
};
|
||||
let start_display = if run.start_time.len() > 23 {
|
||||
run.start_time[..23].to_string()
|
||||
} else {
|
||||
run.start_time.clone()
|
||||
};
|
||||
let start_display = run
|
||||
.start_time_dt
|
||||
.as_ref()
|
||||
.map(format_relative_time)
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let duration_display = run
|
||||
.duration_ms
|
||||
.map(|ms| super::progress::format_duration_short(Duration::from_millis(ms)))
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let cost_display = run
|
||||
.total_cost
|
||||
.map(super::format_cost)
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let status_display = style_status(&run.status, styles);
|
||||
println!(
|
||||
"{:<30} {:<25} {:<10} {:<25} {}",
|
||||
run_id_display, run.workflow_name, run.status, start_display, labels_str
|
||||
"{:<30} {:<25} {:<17} {:<10} {:<10} {:<10} {}",
|
||||
styles.dim.apply_to(&run_id_display),
|
||||
run.workflow_name,
|
||||
status_display,
|
||||
start_display,
|
||||
duration_display,
|
||||
cost_display,
|
||||
styles.dim.apply_to(&labels_str),
|
||||
);
|
||||
}
|
||||
eprintln!("\n{} run(s) listed.", filtered.len());
|
||||
|
||||
if total > limit {
|
||||
eprintln!(
|
||||
"\nShowing {} of {} run(s). Use --all to see all.",
|
||||
limit, total
|
||||
);
|
||||
} else {
|
||||
eprintln!("\n{} run(s) listed.", total);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -830,6 +931,8 @@ mod tests {
|
|||
status: RunStatus::Concluded(crate::outcome::StageStatus::Success),
|
||||
start_time: "2025-06-01T00:00:00Z".into(),
|
||||
labels: HashMap::new(),
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
start_time_dt: None,
|
||||
end_time: None,
|
||||
path: PathBuf::from("/tmp/d1"),
|
||||
|
|
@ -843,6 +946,8 @@ mod tests {
|
|||
status: RunStatus::Concluded(crate::outcome::StageStatus::Success),
|
||||
start_time: "2026-03-01T00:00:00Z".into(),
|
||||
labels: HashMap::new(),
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
start_time_dt: None,
|
||||
end_time: None,
|
||||
path: PathBuf::from("/tmp/d2"),
|
||||
|
|
@ -865,6 +970,8 @@ mod tests {
|
|||
status: RunStatus::Concluded(crate::outcome::StageStatus::Success),
|
||||
start_time: "2026-01-01T00:00:00Z".into(),
|
||||
labels: HashMap::new(),
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
start_time_dt: None,
|
||||
end_time: None,
|
||||
path: PathBuf::from("/tmp/d1"),
|
||||
|
|
@ -878,6 +985,8 @@ mod tests {
|
|||
status: RunStatus::Concluded(crate::outcome::StageStatus::Success),
|
||||
start_time: "2026-01-01T00:00:00Z".into(),
|
||||
labels: HashMap::new(),
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
start_time_dt: None,
|
||||
end_time: None,
|
||||
path: PathBuf::from("/tmp/d2"),
|
||||
|
|
@ -900,6 +1009,8 @@ mod tests {
|
|||
status: RunStatus::Concluded(crate::outcome::StageStatus::Success),
|
||||
start_time: "2026-01-01T00:00:00Z".into(),
|
||||
labels: HashMap::from([("env".into(), "prod".into())]),
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
start_time_dt: None,
|
||||
end_time: None,
|
||||
path: PathBuf::from("/tmp/d1"),
|
||||
|
|
@ -913,6 +1024,8 @@ mod tests {
|
|||
status: RunStatus::Concluded(crate::outcome::StageStatus::Success),
|
||||
start_time: "2026-01-01T00:00:00Z".into(),
|
||||
labels: HashMap::from([("env".into(), "staging".into())]),
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
start_time_dt: None,
|
||||
end_time: None,
|
||||
path: PathBuf::from("/tmp/d2"),
|
||||
|
|
@ -940,6 +1053,8 @@ mod tests {
|
|||
status: RunStatus::Unknown,
|
||||
start_time: "".into(),
|
||||
labels: HashMap::new(),
|
||||
duration_ms: None,
|
||||
total_cost: None,
|
||||
start_time_dt: None,
|
||||
end_time: None,
|
||||
path: PathBuf::from("/tmp/d1"),
|
||||
|
|
@ -1877,4 +1992,4 @@ mod tests {
|
|||
"10-day-old run should be pruned with 7d threshold"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue