cargo-governor 2.0.0

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Pre-check service - business logic for pre-publish checks

use crate::cli::{CheckOpts, OutputFormat};
use crate::error::{CommandExitCode, Result};
use command_group::CommandGroup;
use serde_json::json;
use std::path::PathBuf;

/// Service for running pre-publish checks
pub struct PreCheckService {
    workspace_path: String,
    opts: CheckOpts,
}

impl PreCheckService {
    pub const fn new(workspace_path: String, opts: CheckOpts) -> Self {
        Self {
            workspace_path,
            opts,
        }
    }

    pub fn execute(&self, _format: OutputFormat) -> Result<CommandExitCode> {
        let start_time = std::time::Instant::now();

        let checks_to_run = self.get_checks_to_run();
        let (results, all_passed) = self.run_all_checks(&checks_to_run)?;

        let passed_count = results.iter().filter(|r| r["passed"] == true).count();
        let failed_count = results.len() - passed_count;

        let response = json!({
            "success": all_passed,
            "command": "check",
            "result": {
                "checks": results,
                "summary": {
                    "total": results.len(),
                    "passed": passed_count,
                    "failed": failed_count,
                }
            },
            "metrics": {
                "execution_time_ms": start_time.elapsed().as_millis(),
            }
        });

        println!("{}", serde_json::to_string_pretty(&response).unwrap());

        if all_passed {
            Ok(CommandExitCode::Success)
        } else {
            Ok(CommandExitCode::CheckFailed)
        }
    }

    fn get_checks_to_run(&self) -> Vec<String> {
        self.opts.checks.as_ref().map_or_else(
            || vec!["test".to_string(), "clippy".to_string(), "fmt".to_string()],
            |s| s.split(',').map(|s| s.trim().to_string()).collect(),
        )
    }

    fn run_all_checks(&self, checks_to_run: &[String]) -> Result<(Vec<serde_json::Value>, bool)> {
        let mut results = Vec::new();
        let mut all_passed = true;

        for check in checks_to_run {
            let result = self.run_check(check);
            let passed = result.is_ok();
            let message = result
                .as_ref()
                .map_or_else(std::string::ToString::to_string, |()| "Passed".to_string());

            if !passed && self.opts.fail_fast {
                let response = vec![json!({
                    "name": check,
                    "passed": false,
                    "message": message,
                })];
                return Ok((response, false));
            }

            results.push(json!({
                "name": check,
                "passed": passed,
                "message": message,
            }));

            if !passed {
                all_passed = false;
            }
        }

        Ok((results, all_passed))
    }

    fn run_check(&self, check: &str) -> Result<()> {
        match check {
            "test" => {
                self.run_command(&["cargo", "test", "--all-features"])?;
                Ok(())
            }
            "clippy" => {
                self.run_command(&[
                    "cargo",
                    "clippy",
                    "--all-targets",
                    "--all-features",
                    "--",
                    "-D",
                    "warnings",
                ])?;
                Ok(())
            }
            "fmt" => {
                self.run_command(&["cargo", "fmt", "--check"])?;
                Ok(())
            }
            "doc" => {
                self.run_command(&["cargo", "doc", "--no-deps", "--all-features"])?;
                Ok(())
            }
            "build" => {
                self.run_command(&["cargo", "build", "--all-features"])?;
                Ok(())
            }
            _ => Err(crate::error::Error::Config(format!(
                "Unknown check: {check}"
            ))),
        }
    }

    fn run_command(&self, args: &[&str]) -> Result<std::process::ExitStatus> {
        let output = std::process::Command::new(args[0])
            .args(&args[1..])
            .current_dir(PathBuf::from(&self.workspace_path))
            .group_spawn()
            .map_err(|e| crate::error::Error::Io(format!("Failed to spawn {args:?}: {e}")))?
            .wait()
            .map_err(|e| crate::error::Error::Io(format!("Failed to wait for {args:?}: {e}")))?;

        if output.success() {
            Ok(output)
        } else {
            Err(crate::error::Error::CheckFailed(format!(
                "Command {:?} failed with exit code {:?}",
                args,
                output.code()
            )))
        }
    }
}