use super::deployment_config::DeploymentConfig;
use super::project_detector::{ProjectDetector, ProjectType};
use std::time::Instant;
use tokio::process::Command;
use tracing::{debug, info};
pub struct DeploymentExecutor {
config: DeploymentConfig,
debug: bool,
}
impl DeploymentExecutor {
pub fn new(config: DeploymentConfig, debug: bool) -> Self {
Self { config, debug }
}
pub async fn deploy(&mut self) -> Result<(), String> {
let start_time: Instant = Instant::now();
self.detect_and_configure_project().await?;
self.ensure_port_available().await?;
self.validate_project_directory().await?;
self.git_reset_and_pull().await?;
if let Some(install_cmd) = &self.config.install_command.clone() {
self.run_install_command(install_cmd).await?;
}
if let Some(build_cmd) = &self.config.build_command.clone() {
self.run_build_command(build_cmd).await?;
}
self.stop_existing_processes().await?;
self.start_application().await?;
self.save_pm2_config().await?;
self.config.save_xbp_config(None).await?;
let elapsed: std::time::Duration = start_time.elapsed();
info!(
"Successfully deployed {} on port {} in {:.2?}",
self.config.app_name, self.config.port, elapsed
);
Ok(())
}
async fn detect_and_configure_project(&mut self) -> Result<(), String> {
if self.debug {
debug!(
"Detecting project type in: {}",
self.config.app_dir.display()
);
}
let project_type = ProjectDetector::detect_project_type(&self.config.app_dir).await?;
match &project_type {
ProjectType::Docker { .. } => {
info!(" Detected Docker project (Dockerfile found)");
}
ProjectType::DockerCompose {
compose_files,
detected_ports,
} => {
info!(
" Detected Docker Compose project: {}",
compose_files.join(", ")
);
if !detected_ports.is_empty() {
info!(" Detected ports: {:?}", detected_ports);
}
}
ProjectType::Railway {
has_railway_json,
has_railway_toml,
} => {
let mut parts = Vec::new();
if *has_railway_json {
parts.push("railway.json");
}
if *has_railway_toml {
parts.push("railway.toml");
}
info!(" Detected Railway manifest: {}", parts.join(", "));
}
ProjectType::Python {
has_requirements_txt,
has_pyproject_toml,
} => {
let mut parts = Vec::new();
if *has_requirements_txt {
parts.push("requirements.txt");
}
if *has_pyproject_toml {
parts.push("pyproject.toml");
}
info!(" Detected Python project: {}", parts.join(", "));
}
ProjectType::OpenApi { spec_files } => {
info!(" Detected OpenAPI spec: {}", spec_files.join(", "));
}
ProjectType::Terraform { tf_file_count } => {
info!(" Detected Terraform files: {}", tf_file_count);
}
ProjectType::NextJs {
package_json,
has_next_config,
} => {
info!(
" Detected Next.js project: {} v{}",
package_json.name, package_json.version
);
if *has_next_config {
info!(" Next.js configuration found");
}
}
ProjectType::NodeJs { package_json } => {
info!(
" Detected Node.js project: {} v{}",
package_json.name, package_json.version
);
}
ProjectType::Rust { cargo_toml } => {
info!(
" Detected Rust project: {} v{}",
cargo_toml.name, cargo_toml.version
);
}
ProjectType::Unknown => {
info!(" Unknown project type, using default configuration");
}
}
let recommendations = ProjectDetector::get_deployment_recommendations(&project_type);
self.config.merge_with_recommendations(&recommendations);
if self.debug {
debug!("Build command: {:?}", self.config.build_command);
debug!("Start command: {:?}", self.config.start_command);
debug!("Install command: {:?}", self.config.install_command);
}
Ok(())
}
async fn ensure_port_available(&mut self) -> Result<(), String> {
info!(" Checking port {} availability...", self.config.port);
let port_check = Command::new("sh")
.arg("-c")
.arg(format!("sudo fuser {}/tcp", self.config.port))
.output()
.await;
match port_check {
Ok(output) if output.status.success() => {
info!(
" Port {} is in use, attempting to kill process...",
self.config.port
);
let kill_output = Command::new("sh")
.arg("-c")
.arg(format!("sudo fuser -k {}/tcp", self.config.port))
.output()
.await
.map_err(|e| {
format!("Failed to kill process on port {}: {}", self.config.port, e)
})?;
if kill_output.status.success() {
info!(" Successfully killed process on port {}", self.config.port);
} else {
info!(" Failed to kill process, searching for available port...");
let new_port = self.find_available_port().await?;
self.config.update_port(new_port);
info!(" Found available port: {}", new_port);
}
}
_ => {
info!(" Port {} is available", self.config.port);
}
}
Ok(())
}
async fn find_available_port(&self) -> Result<u16, String> {
for port in 1025..=65535 {
let check_output = Command::new("sh")
.arg("-c")
.arg(format!("sudo fuser {}/tcp", port))
.output()
.await;
if let Ok(output) = check_output {
if !output.status.success() {
return Ok(port);
}
}
}
Err("No available ports found in range 1025-65535".to_string())
}
async fn validate_project_directory(&self) -> Result<(), String> {
if !self.config.app_dir.exists() {
return Err(format!(
"Project directory does not exist: {}",
self.config.app_dir.display()
));
}
if !self.config.app_dir.is_dir() {
return Err(format!(
"Project path is not a directory: {}",
self.config.app_dir.display()
));
}
info!(" Project directory: {}", self.config.app_dir.display());
Ok(())
}
async fn git_reset_and_pull(&self) -> Result<(), String> {
info!(" Resetting git repository...");
let reset_output = Command::new("git")
.arg("reset")
.arg("--hard")
.current_dir(&self.config.app_dir)
.output()
.await
.map_err(|e| format!("Failed to execute git reset: {}", e))?;
if !reset_output.status.success() {
return Err(format!(
"Git reset failed: {}",
String::from_utf8_lossy(&reset_output.stderr)
));
}
info!(" Pulling latest changes...");
let pull_output = Command::new("git")
.arg("pull")
.arg("origin")
.arg("main")
.current_dir(&self.config.app_dir)
.output()
.await
.map_err(|e| format!("Failed to execute git pull: {}", e))?;
if !pull_output.status.success() {
return Err(format!(
"Git pull failed: {}",
String::from_utf8_lossy(&pull_output.stderr)
));
}
info!(" Git operations completed");
Ok(())
}
async fn run_install_command(&self, install_cmd: &str) -> Result<(), String> {
info!(" Installing dependencies: {}", install_cmd);
let install_output = Command::new("sh")
.arg("-c")
.arg(install_cmd)
.envs(&self.config.environment)
.current_dir(&self.config.app_dir)
.output()
.await
.map_err(|e| format!("Failed to execute install command: {}", e))?;
if !install_output.status.success() {
return Err(format!(
"Install command failed: {}",
String::from_utf8_lossy(&install_output.stderr)
));
}
info!(" Dependencies installed successfully");
Ok(())
}
async fn run_build_command(&self, build_cmd: &str) -> Result<(), String> {
info!(" Building project: {}", build_cmd);
let build_output = Command::new("sh")
.arg("-c")
.arg(build_cmd)
.envs(&self.config.environment)
.current_dir(&self.config.app_dir)
.output()
.await
.map_err(|e| format!("Failed to execute build command: {}", e))?;
if !build_output.status.success() {
return Err(format!(
"Build command failed: {}",
String::from_utf8_lossy(&build_output.stderr)
));
}
info!("Project built successfully");
Ok(())
}
async fn stop_existing_processes(&self) -> Result<(), String> {
info!("Stopping existing processes...");
let pm2_stop = Command::new("pm2")
.arg("stop")
.arg(&self.config.app_name)
.output()
.await;
match pm2_stop {
Ok(output) if output.status.success() => {
info!(" Stopped PM2 process: {}", self.config.app_name);
}
_ => {
info!(" No existing PM2 process found");
}
}
let kill_port = Command::new("sh")
.arg("-c")
.arg(format!("sudo fuser -k {}/tcp", self.config.port))
.output()
.await;
match kill_port {
Ok(output) if output.status.success() => {
info!(" Killed processes on port {}", self.config.port);
}
_ => {
info!(" No processes found on port {}", self.config.port);
}
}
Ok(())
}
async fn start_application(&self) -> Result<(), String> {
let start_cmd = self
.config
.start_command
.as_ref()
.ok_or("No start command configured")?;
info!(" Starting application: {}", start_cmd);
let pm2_cmd = format!("{} --port {}", start_cmd, self.config.port);
let start_output = Command::new("pm2")
.arg("start")
.arg(&pm2_cmd)
.arg("--name")
.arg(&self.config.app_name)
.arg("--")
.arg("--port")
.arg(&self.config.port.to_string())
.envs(&self.config.environment)
.current_dir(&self.config.app_dir)
.output()
.await
.map_err(|e| format!("Failed to start PM2 process: {}", e))?;
if !start_output.status.success() {
return Err(format!(
"Failed to start application: {}",
String::from_utf8_lossy(&start_output.stderr)
));
}
info!("Application started successfully");
Ok(())
}
async fn save_pm2_config(&self) -> Result<(), String> {
info!("Saving PM2 configuration...");
let save_output = Command::new("pm2")
.arg("save")
.output()
.await
.map_err(|e| format!("Failed to save PM2 config: {}", e))?;
if !save_output.status.success() {
return Err(format!(
"PM2 save failed: {}",
String::from_utf8_lossy(&save_output.stderr)
));
}
info!("PM2 configuration saved");
Ok(())
}
}