use std::env;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::process::Command;
use crate::commands::pm2::{pm2_cleanup, pm2_delete, pm2_save, pm2_start, pm2_stop};
use crate::commands::service::load_xbp_config_with_root;
use crate::logging::{log_debug, log_info, log_success};
use crate::strategies::get_service_by_name;
use crate::utils::version::{fetch_version, increment_version};
pub async fn run_redeploy_service(service_name: &str, debug: bool) -> Result<(), String> {
let _ = log_info(
"redeploy",
&format!("Starting redeploy for service '{}'", service_name),
None,
)
.await;
let (project_root, config) = load_xbp_config_with_root().await?;
let service = get_service_by_name(&config, service_name)?;
let envs = merge_envs(config.environment.as_ref(), service.environment.as_ref());
let working_dir = if service.force_run_from_root.unwrap_or(false) {
project_root.clone()
} else if let Some(root_dir) = &service.root_directory {
if root_dir.starts_with('/') {
PathBuf::from(root_dir)
} else {
project_root.join(root_dir)
}
} else {
project_root.clone()
};
let _ = log_info(
"redeploy",
&format!("Working directory: {}", working_dir.display()),
None,
)
.await;
let _ = log_info("redeploy", "Resetting local changes...", None).await;
let mut git_reset = Command::new("git");
git_reset.arg("reset").arg("--hard");
git_reset.current_dir(&working_dir);
let reset_output = git_reset
.output()
.await
.map_err(|e| format!("Failed to reset git changes: {}", e))?;
if !reset_output.status.success() {
return Err(format!(
"Git reset failed: {}",
String::from_utf8_lossy(&reset_output.stderr)
));
}
let _ = log_info("redeploy", "Pulling latest changes...", None).await;
let mut git_pull = Command::new("git");
git_pull.arg("pull").arg("origin").arg(&service.branch);
git_pull.current_dir(&working_dir);
let pull_output = git_pull
.output()
.await
.map_err(|e| format!("Failed to pull git changes: {}", e))?;
if !pull_output.status.success() {
return Err(format!(
"Git pull failed: {}",
String::from_utf8_lossy(&pull_output.stderr)
));
}
let _ = log_info("redeploy", "Fetching version from API...", None).await;
let version = fetch_version(&service.name).await?;
let _ = log_info("redeploy", &format!("Version: {}", version), None).await;
let username = env::var("USER")
.or_else(|_| env::var("USERNAME"))
.unwrap_or_else(|_| "unknown".to_string());
let dist_base = PathBuf::from(format!("/home/{}/.xbp/dist", username));
let dist_folder = dist_base.join(&version);
std::fs::create_dir_all(&dist_folder)
.map_err(|e| format!("Failed to create dist folder: {}", e))?;
let _ = log_info(
"redeploy",
&format!("Dist folder: {}", dist_folder.display()),
None,
)
.await;
if let Some(commands) = &service.commands {
if let Some(pre_cmd) = &commands.pre {
if !pre_cmd.is_empty() {
let _ = log_info("redeploy", "Running pre-command...", Some(pre_cmd)).await;
run_command_in_dir(&working_dir, pre_cmd, envs.as_ref(), debug).await?;
}
}
}
if let Some(commands) = &service.commands {
if let Some(install_cmd) = &commands.install {
if !install_cmd.is_empty() {
let _ = log_info("redeploy", "Installing dependencies...", Some(install_cmd)).await;
run_command_in_dir(&working_dir, install_cmd, envs.as_ref(), debug).await?;
}
}
}
if let Some(commands) = &service.commands {
if let Some(build_cmd) = &commands.build {
if !build_cmd.is_empty() {
let _ = log_info("redeploy", "Building...", Some(build_cmd)).await;
run_command_in_dir(&working_dir, build_cmd, envs.as_ref(), debug).await?;
} else {
let _ = log_info("redeploy", "Build command is empty, skipping", None).await;
}
}
}
let _ = log_info(
"redeploy",
&format!("Stopping existing PM2 process '{}'...", service.name),
None,
)
.await;
pm2_stop(&service.name, debug).await?;
match service.target.as_str() {
"rust" => {
let possible_binary_names = vec![
&service.name,
&config.project_name,
config.crate_name.as_deref().unwrap_or(&service.name),
];
for binary_name in possible_binary_names {
let binary_path = working_dir.join("target").join("release").join(binary_name);
if binary_path.exists() {
let dist_binary = dist_folder.join(format!("{}-binary", service.name));
std::fs::copy(&binary_path, &dist_binary)
.map_err(|e| format!("Failed to copy binary: {}", e))?;
let _ = log_info(
"redeploy",
&format!("Copied binary to {}", dist_binary.display()),
None,
)
.await;
break;
}
}
}
"nextjs" | "expressjs" => {
let _ = log_info("redeploy", "Node.js build artifacts in place", None).await;
}
"python" => {
let _ = log_info("redeploy", "Python service ready", None).await;
}
_ => {}
}
let log_dir = project_root.join(".xbp").join("logs").join(&service.name);
std::fs::create_dir_all(&log_dir)
.map_err(|e| format!("Failed to create log directory: {}", e))?;
if let Some(commands) = &service.commands {
if let Some(start_cmd) = &commands.start {
if !start_cmd.is_empty() {
let pm2_command = if start_cmd.contains("--port") {
start_cmd.clone()
} else {
format!("{} --port {}", start_cmd, service.port)
};
let _ = log_info(
"redeploy",
&format!("Starting service with PM2: {}", pm2_command),
None,
)
.await;
pm2_delete(&service.name, debug).await.ok();
let envs = merge_envs(config.environment.as_ref(), service.environment.as_ref());
pm2_start(
&service.name,
&pm2_command,
Some(&log_dir),
envs.as_ref(),
debug,
)
.await?;
let _ = log_success(
"redeploy",
&format!("Service '{}' started successfully", service.name),
None,
)
.await;
}
}
}
pm2_save(debug).await?;
let _ = log_info("redeploy", "Updating version in database...", None).await;
increment_version(&service.name, "minor").await?;
let _ = log_info("redeploy", "Cleaning up PM2 processes...", None).await;
pm2_cleanup(debug).await?;
let _ = log_success(
"redeploy",
&format!("Redeploy completed for service '{}'", service_name),
None,
)
.await;
Ok(())
}
fn merge_envs(
global: Option<&std::collections::HashMap<String, String>>,
service: Option<&std::collections::HashMap<String, String>>,
) -> Option<std::collections::HashMap<String, String>> {
if global.is_none() && service.is_none() {
return None;
}
let mut out = std::collections::HashMap::new();
if let Some(g) = global {
out.extend(g.clone());
}
if let Some(s) = service {
out.extend(s.clone());
}
Some(out)
}
async fn run_command_in_dir(
dir: &PathBuf,
command: &str,
envs: Option<&std::collections::HashMap<String, String>>,
_debug: bool,
) -> Result<(), String> {
let _ = log_debug(
"redeploy",
&format!("Executing: {} in {}", command, dir.display()),
None,
)
.await;
#[cfg(unix)]
let mut cmd = Command::new("sh");
#[cfg(unix)]
cmd.arg("-c");
#[cfg(windows)]
let mut cmd = Command::new("cmd");
#[cfg(windows)]
cmd.arg("/C");
cmd.arg(command);
cmd.current_dir(dir);
if let Some(envs) = envs {
cmd.envs(envs);
}
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let status = cmd
.status()
.await
.map_err(|e| format!("Failed to execute command: {}", e))?;
if !status.success() {
return Err(format!(
"Command failed with exit code: {}",
status.code().unwrap_or(-1)
));
}
Ok(())
}