//! Atomic symlink-swap deploys. //! //! Layout on every target (MM, A nodes, B nodes, ...): //! //! / //! releases/ //! 0.8.1/ //! server <- the binary //! 0.8.2/ //! server //! current -> releases/0.8.2 //! //! `ln -sfn` makes the swap atomic on Linux. systemd units should point at //! `/current/server` so a swap + reload picks up the new binary //! without a window where the unit references a missing path. //! //! v0 only implements local deploys (used for MM and for localhost-dev //! "remote" nodes whose ssh_target is `local`). Real SSH/rsync deploys are //! follow-up work — see the `remote_deploy_stub` branch. use crate::topology::Node; use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; use tokio::process::Command; pub async fn deploy_local(release_root: &Path, version: &str, binary: &Path) -> Result { let release_dir = release_root.join("releases").join(version); tokio::fs::create_dir_all(&release_dir).await?; let dest = release_dir.join("server"); tokio::fs::copy(binary, &dest) .await .with_context(|| format!("copy {} -> {}", binary.display(), dest.display()))?; let current = release_root.join("current"); // ln -sfn is atomic on Linux; on macOS the dev path is non-prod so the // race is irrelevant. We shell out rather than using std::os::unix::fs // symlink + rename because the rename-over-symlink pattern is platform-fussy. let target = format!("releases/{version}"); let out = Command::new("ln") .args(["-sfn", &target]) .arg(¤t) .output() .await?; anyhow::ensure!( out.status.success(), "symlink swap failed: {}", String::from_utf8_lossy(&out.stderr), ); Ok(release_dir) } pub async fn deploy_node(node: &Node, version: &str, binary: &Path) -> Result { if node.ssh_target == "local" || node.ssh_target.is_empty() { return deploy_local(Path::new(&node.release_root), version, binary).await; } remote_deploy_stub(node, version, binary).await } async fn remote_deploy_stub(node: &Node, version: &str, _binary: &Path) -> Result { // Real implementation: rsync the binary to :/releases//server, // then ssh "ln -sfn releases/ current && systemctl reload-or-restart ". // Wiring this up needs a story for systemd unit naming and ssh key/auth conventions; deferring // until the localhost smoke loop is settled and we know which knobs matter. anyhow::bail!( "remote deploy not yet implemented (node {} -> {}); use ssh_target=local for dev", node.name, node.ssh_target, ); #[allow(unreachable_code)] Ok(PathBuf::from(&node.release_root).join("releases").join(version)) }