cargo-governor 2.0.0

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Resume service - business logic for resuming interrupted releases

use crate::cli::{OutputFormat, ResumeOpts};
use crate::error::{CommandExitCode, Result};
use governor_checkpoint::FileCheckpointStore;
use governor_core::traits::checkpoint_store::{Checkpoint, CheckpointStore};
use serde_json::json;
use std::path::PathBuf;

/// Service for resuming interrupted releases
pub struct ResumeService {
    workspace_path: String,
    opts: ResumeOpts,
}

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

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

        if self.opts.list {
            return self.list_checkpoints();
        }

        if self.opts.clean {
            return self.clean_checkpoints();
        }

        let checkpoint_dir = self.get_checkpoint_dir();
        let checkpoint_id = self.get_checkpoint_id().await?;

        let store = FileCheckpointStore::new(checkpoint_dir).map_err(|e| {
            crate::error::Error::Config(format!("Failed to create checkpoint store: {e}"))
        })?;

        let checkpoint = self.load_checkpoint(&store).await?;

        if checkpoint.id != checkpoint_id {
            return Err(crate::error::Error::Config(format!(
                "Checkpoint mismatch: expected {}, found {}",
                checkpoint_id, checkpoint.id
            )));
        }

        self.print_response(&checkpoint, start_time)?;
        Ok(CommandExitCode::Success)
    }

    fn get_checkpoint_dir(&self) -> PathBuf {
        PathBuf::from(&self.workspace_path)
            .join(".cargo-governor")
            .join("checkpoints")
    }

    async fn get_checkpoint_id(&self) -> Result<String> {
        if let Some(ref id) = self.opts.checkpoint {
            return Ok(id.clone());
        }

        let checkpoint_dir = self.get_checkpoint_dir();
        let store = FileCheckpointStore::new(checkpoint_dir).map_err(|e| {
            crate::error::Error::Config(format!("Failed to create checkpoint store: {e}"))
        })?;

        if let Some(checkpoint) = store
            .load()
            .await
            .map_err(|e| crate::error::Error::Config(format!("Failed to load checkpoint: {e}")))?
        {
            Ok(checkpoint.id)
        } else {
            Err(crate::error::Error::Config(
                "No checkpoint found to resume from".to_string(),
            ))
        }
    }

    async fn load_checkpoint(&self, store: &FileCheckpointStore) -> Result<Checkpoint> {
        store
            .load()
            .await
            .map_err(|e| crate::error::Error::Config(format!("Failed to load checkpoint: {e}")))?
            .ok_or_else(|| crate::error::Error::Config("Checkpoint not found".to_string()))
    }

    fn print_response(
        &self,
        checkpoint: &Checkpoint,
        start_time: std::time::Instant,
    ) -> Result<()> {
        let response = json!({
            "success": true,
            "command": "resume",
            "workspace": self.workspace_path,
            "result": {
                "checkpoint": {
                    "id": checkpoint.id,
                    "workflow_id": checkpoint.workflow_id,
                    "step_index": checkpoint.step_index,
                    "completed_steps": checkpoint.completed_steps,
                    "timestamp": checkpoint.timestamp.to_rfc3339(),
                },
                "action": "To resume, run: cargo-governor release --resume",
            },
            "metrics": {
                "execution_time_ms": start_time.elapsed().as_millis(),
            }
        });

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

    fn list_checkpoints(&self) -> Result<CommandExitCode> {
        let checkpoint_dir = self.get_checkpoint_dir();

        if !checkpoint_dir.exists() {
            let response = json!({
                "success": true,
                "command": "resume",
                "result": {
                    "message": "No checkpoints found",
                    "checkpoints": [],
                }
            });
            println!("{}", serde_json::to_string_pretty(&response).unwrap());
            return Ok(CommandExitCode::Success);
        }

        let mut checkpoints = self.load_all_checkpoints(&checkpoint_dir)?;

        checkpoints.sort_by(|a, b| {
            let a_time = a["timestamp"].as_str().unwrap_or("");
            let b_time = b["timestamp"].as_str().unwrap_or("");
            b_time.cmp(a_time)
        });

        let response = json!({
            "success": true,
            "command": "resume",
            "result": {
                "checkpoints": checkpoints,
                "count": checkpoints.len(),
            }
        });

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

    fn load_all_checkpoints(&self, checkpoint_dir: &PathBuf) -> Result<Vec<serde_json::Value>> {
        let mut checkpoints = Vec::new();

        let entries = std::fs::read_dir(checkpoint_dir).map_err(|e| {
            crate::error::Error::Io(format!("Failed to read checkpoint directory: {e}"))
        })?;

        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|s| s.to_str()) == Some("json") {
                let content = std::fs::read_to_string(&path).map_err(|e| {
                    crate::error::Error::Io(format!("Failed to read checkpoint file: {e}"))
                })?;

                if let Ok(checkpoint) = serde_json::from_str::<Checkpoint>(&content) {
                    checkpoints.push(json!({
                        "id": checkpoint.id,
                        "workflow_id": checkpoint.workflow_id,
                        "step_index": checkpoint.step_index,
                        "completed_steps": checkpoint.completed_steps,
                        "timestamp": checkpoint.timestamp.to_rfc3339(),
                        "resume_command": format!("cargo-governor release --resume --checkpoint {}", checkpoint.id),
                    }));
                }
            }
        }

        Ok(checkpoints)
    }

    fn clean_checkpoints(&self) -> Result<CommandExitCode> {
        let checkpoint_dir = self.get_checkpoint_dir();

        if !checkpoint_dir.exists() {
            let response = json!({
                "success": true,
                "command": "resume",
                "result": {
                    "message": "No checkpoints to clean",
                    "removed": 0,
                }
            });
            println!("{}", serde_json::to_string_pretty(&response).unwrap());
            return Ok(CommandExitCode::Success);
        }

        let removed = self.remove_old_checkpoints(&checkpoint_dir)?;

        let response = json!({
            "success": true,
            "command": "resume",
            "result": {
                "message": format!("Removed {} old checkpoints", removed),
                "removed": removed,
            }
        });

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

    fn remove_old_checkpoints(&self, checkpoint_dir: &PathBuf) -> Result<usize> {
        let mut removed = 0usize;

        let entries = std::fs::read_dir(checkpoint_dir).map_err(|e| {
            crate::error::Error::Io(format!("Failed to read checkpoint directory: {e}"))
        })?;

        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|s| s.to_str()) == Some("json")
                && let Ok(metadata) = std::fs::metadata(&path)
                && let Ok(modified) = metadata.modified()
            {
                let age = chrono::Utc::now()
                    .signed_duration_since(chrono::DateTime::<chrono::Utc>::from(modified));

                if age.num_days() > 30 {
                    std::fs::remove_file(&path).map_err(|e| {
                        crate::error::Error::Io(format!("Failed to remove checkpoint: {e}"))
                    })?;
                    removed = removed.saturating_add(1);
                }
            }
        }

        Ok(removed)
    }
}