Files
xboct-deploy/src/deploy.rs
T

153 lines
4.1 KiB
Rust

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<ssh::Session> {
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())
}
}