use std::time::{Duration, Instant}; use anyhow::Result; use colored::Colorize; use crate::config::Project; use crate::{local, ssh, sync}; pub fn deploy(name: &str, project: &Project, dry_run: bool) -> Result<()> { println!("deploying '{name}'"); let local_cmds = project .local .as_ref() .map_or([].as_slice(), |c| c.commands.as_slice()); let remote_cmds = project .remote .as_ref() .map_or([].as_slice(), |c| c.commands.as_slice()); if dry_run { print_plan(project, local_cmds, remote_cmds); return Ok(()); } if local_cmds.is_empty() && project.sync.is_empty() && remote_cmds.is_empty() { println!("nothing to do: no local commands, sync rules or remote commands"); return Ok(()); } let total = Instant::now(); for cmd in local_cmds { let step = Instant::now(); if let Err(err) = local::execute(cmd, &project.workdir) { fail_line("🟢 [Local]", &format!("running: {cmd}..."), err.to_string()); return Err(err); } success_line( "🟢 [Local]", &format!("running: {cmd}..."), "Success!", step.elapsed(), ); } if !project.sync.is_empty() || !remote_cmds.is_empty() { let mut session = connect(project)?; for rule in &project.sync { let step = Instant::now(); let target = rule.target.to_string_lossy().to_string(); let result = sync::upload(&mut session, &rule.source, &target); let label = format!("transferring {} to {}...", rule.source.display(), target); match result { Ok(stats) => success_line( "🔵 [SFTP]", &label, &format!("Done! ({stats})"), step.elapsed(), ), Err(err) => { fail_line("🔵 [SFTP]", &label, err.to_string()); return Err(err); } } } for cmd in remote_cmds { let step = Instant::now(); if let Err(err) = session.exec(cmd) { fail_line( "🟡 [Remote]", &format!("running: {cmd}..."), err.to_string(), ); return Err(err); } success_line( "🟡 [Remote]", &format!("running: {cmd}..."), "Success!", step.elapsed(), ); } } println!( "🎉 {} (total {})", "Deploy successful!".green().bold(), fmt_dur(total.elapsed()) ); Ok(()) } fn print_plan(project: &Project, local_cmds: &[String], remote_cmds: &[String]) { println!("dry-run plan:"); for cmd in local_cmds { println!(" 🟢 [Local] {cmd}"); } for rule in &project.sync { println!( " 🔵 [SFTP] {} -> {}", rule.source.display(), rule.target.display() ); } for cmd in remote_cmds { println!(" 🟡 [Remote] {cmd}"); } println!("{}", "nothing executed".dimmed()); } fn connect(project: &Project) -> Result { let target = ssh::Target { host_alias: project.host.clone(), user: project.user.clone(), port: project.port, key_path: project.key_path.clone(), }; let resolved = ssh::resolve(&target)?; ssh::connect(&resolved) } fn success_line(tag: &str, action: &str, status: &str, took: Duration) { println!( "{} {} {} ({})", tag.green().bold(), action, status.green(), fmt_dur(took).dimmed() ); } fn fail_line(tag: &str, action: &str, message: String) { println!( "{} {} {}", tag.green().bold(), action, "FAILED".red().bold() ); eprintln!("{}", message.red()); } fn fmt_dur(d: Duration) -> String { let ms = d.as_millis(); if ms < 1000 { format!("{ms}ms") } else { format!("{:.2}s", d.as_secs_f64()) } }