Skip to main content

batuta/stack/releaser_preflight/
helpers.rs

1//! Shared helper functions for preflight checks
2//!
3//! This module contains free functions used by multiple preflight check methods.
4
5use crate::stack::releaser::ReleaseOrchestrator;
6use crate::stack::types::PreflightCheck;
7use std::path::Path;
8use std::process::Command;
9
10/// Execute an external command described by a single whitespace-separated
11/// config string and dispatch the result through a caller-provided closure.
12///
13/// Handles the three outcomes every check shares:
14///   1. Empty command string  -> pass with `skip_msg`
15///   2. Command not found     -> pass with "<tool> not found (skipped)"
16///   3. Other spawn error     -> fail with error details
17///   4. Successful spawn      -> delegate to `process_output`
18pub fn run_check_command<F>(
19    config_command: &str,
20    check_id: &str,
21    skip_msg: &str,
22    crate_path: &Path,
23    process_output: F,
24) -> PreflightCheck
25where
26    F: FnOnce(&std::process::Output, &str, &str) -> PreflightCheck,
27{
28    let parts: Vec<&str> = config_command.split_whitespace().collect();
29    if parts.is_empty() {
30        return PreflightCheck::pass(check_id, skip_msg);
31    }
32    match Command::new(parts[0])
33        .args(parts.get(1..).unwrap_or(&[]))
34        .current_dir(crate_path)
35        .output()
36    {
37        Ok(output) => {
38            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
39            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
40            process_output(&output, &stdout, &stderr)
41        }
42        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
43            PreflightCheck::pass(check_id, format!("{} not found (skipped)", parts[0]))
44        }
45        Err(e) => PreflightCheck::fail(check_id, format!("Failed to run {}: {}", parts[0], e)),
46    }
47}
48
49/// Try several JSON keys in order and return the first successfully parsed f64.
50pub fn parse_value_from_json(json: &str, keys: &[&str]) -> Option<f64> {
51    keys.iter().find_map(|k| ReleaseOrchestrator::parse_score_from_json(json, k))
52}
53
54/// Try several JSON keys in order and return the first successfully parsed u32.
55pub fn parse_count_from_json_multi(json: &str, keys: &[&str]) -> Option<u32> {
56    parse_value_from_json(json, keys).map(|f| f as u32)
57}
58
59/// Evaluate a numeric score against a threshold, producing a uniform
60/// pass / fail / warning [`PreflightCheck`].
61///
62/// Used by TDG, Popper, and similar score-based gates.
63pub fn score_check_result(
64    check_id: &str,
65    label: &str,
66    value: Option<f64>,
67    threshold: f64,
68    fail_on_threshold: bool,
69    status_success: bool,
70) -> PreflightCheck {
71    match value {
72        Some(v) if v >= threshold => PreflightCheck::pass(
73            check_id,
74            format!("{}: {:.1} (minimum: {:.1})", label, v, threshold),
75        ),
76        Some(v) if fail_on_threshold => PreflightCheck::fail(
77            check_id,
78            format!("{} {:.1} below minimum {:.1}", label, v, threshold),
79        ),
80        Some(v) => PreflightCheck::pass(
81            check_id,
82            format!("{}: {:.1} (warning: below {:.1})", label, v, threshold),
83        ),
84        None if status_success => PreflightCheck::pass(check_id, format!("{} check passed", label)),
85        None => {
86            PreflightCheck::pass(check_id, format!("{} check completed (score not parsed)", label))
87        }
88    }
89}