mecha10-deploy 0.6.5

Mecha10 deploy services — Docker management, binary distribution, remote deployment
Documentation
//! Deployment service types

use mecha10_core::enum_str_method;
use std::path::PathBuf;
use std::str::FromStr;

/// Deployment configuration
#[derive(Debug, Clone)]
pub struct DeployConfig {
    pub host: String,
    pub user: String,
    pub port: u16,
    pub ssh_key: Option<PathBuf>,
    pub remote_dir: String,
    pub service_name: String,
    pub backup_dir: String,
    pub health_check_timeout: u64,
    pub health_check_retries: u32,
}

impl Default for DeployConfig {
    fn default() -> Self {
        Self {
            host: "robot.local".to_string(),
            user: "mecha10".to_string(),
            port: 22,
            ssh_key: None,
            remote_dir: "/opt/mecha10".to_string(),
            service_name: "mecha10-robot".to_string(),
            backup_dir: "/opt/mecha10/backups".to_string(),
            health_check_timeout: 30,
            health_check_retries: 3,
        }
    }
}

/// Deployment strategy
///
/// Note: `Rolling` and `BlueGreen` are currently implemented as aliases for
/// `Direct` (stop -> deploy -> start on the single target host) — this crate
/// only manages one remote host/systemd service per `DeployConfig`, so there
/// is no multi-instance or routing infrastructure to actually stage a
/// gradual/blue-green rollout against. A `Canary` variant previously existed
/// here but was removed: it claimed to do a traffic-split partial rollout
/// while actually just calling `deploy_direct`, and there is no
/// proxy/router/multi-host infrastructure in this codebase for a canary
/// deploy to drive. If real canary traffic-splitting is needed, it should be
/// reintroduced alongside the routing infrastructure that would back it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeployStrategy {
    Direct,
    Rolling,
    BlueGreen,
}

impl FromStr for DeployStrategy {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "direct" => Ok(DeployStrategy::Direct),
            "rolling" => Ok(DeployStrategy::Rolling),
            "blue-green" | "bluegreen" => Ok(DeployStrategy::BlueGreen),
            _ => Err(format!("Invalid deployment strategy: {}", s)),
        }
    }
}

enum_str_method!(
    pub fn as_str(DeployStrategy) -> &str {
        Direct => "direct",
        Rolling => "rolling",
        BlueGreen => "blue-green",
    }
);

/// Deployment result
#[derive(Debug, Clone)]
pub struct DeployResult {
    pub success: bool,
    pub message: String,
    pub health_check_passed: bool,
    pub backup_created: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deploy_strategy_parses_remaining_strategies() {
        assert_eq!("direct".parse::<DeployStrategy>(), Ok(DeployStrategy::Direct));
        assert_eq!("rolling".parse::<DeployStrategy>(), Ok(DeployStrategy::Rolling));
        assert_eq!("blue-green".parse::<DeployStrategy>(), Ok(DeployStrategy::BlueGreen));
        assert_eq!("bluegreen".parse::<DeployStrategy>(), Ok(DeployStrategy::BlueGreen));
    }

    #[test]
    fn deploy_strategy_rejects_removed_canary_strategy() {
        // `canary` used to parse into a `DeployStrategy::Canary` variant whose
        // handler was just an alias for `Direct` — no real traffic-splitting
        // ever backed it, and this crate only manages a single remote
        // host/service per `DeployConfig`, so there's no routing
        // infrastructure for a canary rollout to drive. The variant was
        // removed rather than left as a misleading no-op; see the
        // `DeployStrategy` doc comment.
        assert!("canary".parse::<DeployStrategy>().is_err());
    }

    #[test]
    fn deploy_strategy_rejects_unknown_strategy() {
        assert!("invalid".parse::<DeployStrategy>().is_err());
    }

    #[test]
    fn deploy_strategy_as_str_matches_from_str() {
        for (s, expected) in [
            ("direct", DeployStrategy::Direct),
            ("rolling", DeployStrategy::Rolling),
            ("blue-green", DeployStrategy::BlueGreen),
        ] {
            let parsed: DeployStrategy = s.parse().unwrap();
            assert_eq!(parsed, expected);
            assert_eq!(parsed.as_str(), s);
        }
    }
}