1use super::deployment_config::DeploymentConfig;
7use super::project_detector::{ProjectDetector, ProjectType};
8use std::time::Instant;
9use tokio::process::Command;
10use tracing::{debug, info};
11
12pub struct DeploymentExecutor {
13 config: DeploymentConfig,
14 debug: bool,
15}
16
17impl DeploymentExecutor {
18 pub fn new(config: DeploymentConfig, debug: bool) -> Self {
19 Self { config, debug }
20 }
21
22 pub async fn deploy(&mut self) -> Result<(), String> {
24 let start_time: Instant = Instant::now();
25
26 self.detect_and_configure_project().await?;
27 self.ensure_port_available().await?;
28 self.validate_project_directory().await?;
29 self.git_reset_and_pull().await?;
30 if let Some(install_cmd) = &self.config.install_command.clone() {
31 self.run_install_command(install_cmd).await?;
32 }
33 if let Some(build_cmd) = &self.config.build_command.clone() {
34 self.run_build_command(build_cmd).await?;
35 }
36 self.stop_existing_processes().await?;
37 self.start_application().await?;
38 self.save_pm2_config().await?;
39 self.config.save_xbp_config(None).await?;
40
41 let elapsed: std::time::Duration = start_time.elapsed();
42 info!(
43 "Successfully deployed {} on port {} in {:.2?}",
44 self.config.app_name, self.config.port, elapsed
45 );
46
47 Ok(())
48 }
49
50 async fn detect_and_configure_project(&mut self) -> Result<(), String> {
52 if self.debug {
53 debug!(
54 "Detecting project type in: {}",
55 self.config.app_dir.display()
56 );
57 }
58
59 let project_type = ProjectDetector::detect_project_type(&self.config.app_dir).await?;
60
61 match &project_type {
62 ProjectType::Docker { .. } => {
63 info!(" Detected Docker project (Dockerfile found)");
64 }
65 ProjectType::DockerCompose {
66 compose_files,
67 detected_ports,
68 } => {
69 info!(
70 " Detected Docker Compose project: {}",
71 compose_files.join(", ")
72 );
73 if !detected_ports.is_empty() {
74 info!(" Detected ports: {:?}", detected_ports);
75 }
76 }
77 ProjectType::Railway {
78 has_railway_json,
79 has_railway_toml,
80 } => {
81 let mut parts = Vec::new();
82 if *has_railway_json {
83 parts.push("railway.json");
84 }
85 if *has_railway_toml {
86 parts.push("railway.toml");
87 }
88 info!(" Detected Railway manifest: {}", parts.join(", "));
89 }
90 ProjectType::Python {
91 has_requirements_txt,
92 has_pyproject_toml,
93 } => {
94 let mut parts = Vec::new();
95 if *has_requirements_txt {
96 parts.push("requirements.txt");
97 }
98 if *has_pyproject_toml {
99 parts.push("pyproject.toml");
100 }
101 info!(" Detected Python project: {}", parts.join(", "));
102 }
103 ProjectType::OpenApi { spec_files } => {
104 info!(" Detected OpenAPI spec: {}", spec_files.join(", "));
105 }
106 ProjectType::Terraform { tf_file_count } => {
107 info!(" Detected Terraform files: {}", tf_file_count);
108 }
109 ProjectType::NextJs {
110 package_json,
111 has_next_config,
112 } => {
113 info!(
114 " Detected Next.js project: {} v{}",
115 package_json.name, package_json.version
116 );
117 if *has_next_config {
118 info!(" Next.js configuration found");
119 }
120 }
121 ProjectType::NodeJs { package_json } => {
122 info!(
123 " Detected Node.js project: {} v{}",
124 package_json.name, package_json.version
125 );
126 }
127 ProjectType::Rust { cargo_toml } => {
128 info!(
129 " Detected Rust project: {} v{}",
130 cargo_toml.name, cargo_toml.version
131 );
132 }
133 ProjectType::Unknown => {
134 info!(" Unknown project type, using default configuration");
135 }
136 }
137
138 let recommendations =
139 ProjectDetector::get_deployment_recommendations(&self.config.app_dir, &project_type);
140 self.config.merge_with_recommendations(&recommendations);
141
142 if self.debug {
143 debug!("Build command: {:?}", self.config.build_command);
144 debug!("Start command: {:?}", self.config.start_command);
145 debug!("Install command: {:?}", self.config.install_command);
146 }
147
148 Ok(())
149 }
150
151 async fn ensure_port_available(&mut self) -> Result<(), String> {
153 info!(" Checking port {} availability...", self.config.port);
154
155 let port_check = Command::new("sh")
156 .arg("-c")
157 .arg(format!("sudo fuser {}/tcp", self.config.port))
158 .output()
159 .await;
160
161 match port_check {
162 Ok(output) if output.status.success() => {
163 info!(
164 " Port {} is in use, attempting to kill process...",
165 self.config.port
166 );
167
168 let kill_output = Command::new("sh")
169 .arg("-c")
170 .arg(format!("sudo fuser -k {}/tcp", self.config.port))
171 .output()
172 .await
173 .map_err(|e| {
174 format!("Failed to kill process on port {}: {}", self.config.port, e)
175 })?;
176
177 if kill_output.status.success() {
178 info!(" Successfully killed process on port {}", self.config.port);
179 } else {
180 info!(" Failed to kill process, searching for available port...");
181 let new_port = self.find_available_port().await?;
182 self.config.update_port(new_port);
183 info!(" Found available port: {}", new_port);
184 }
185 }
186 _ => {
187 info!(" Port {} is available", self.config.port);
188 }
189 }
190
191 Ok(())
192 }
193
194 async fn find_available_port(&self) -> Result<u16, String> {
196 for port in 1025..=65535 {
197 let check_output = Command::new("sh")
198 .arg("-c")
199 .arg(format!("sudo fuser {}/tcp", port))
200 .output()
201 .await;
202
203 if let Ok(output) = check_output {
204 if !output.status.success() {
205 return Ok(port);
206 }
207 }
208 }
209
210 Err("No available ports found in range 1025-65535".to_string())
211 }
212
213 async fn validate_project_directory(&self) -> Result<(), String> {
215 if !self.config.app_dir.exists() {
216 return Err(format!(
217 "Project directory does not exist: {}",
218 self.config.app_dir.display()
219 ));
220 }
221
222 if !self.config.app_dir.is_dir() {
223 return Err(format!(
224 "Project path is not a directory: {}",
225 self.config.app_dir.display()
226 ));
227 }
228
229 info!(" Project directory: {}", self.config.app_dir.display());
230 Ok(())
231 }
232
233 async fn git_reset_and_pull(&self) -> Result<(), String> {
235 info!(" Resetting git repository...");
236
237 let reset_output = Command::new("git")
238 .arg("reset")
239 .arg("--hard")
240 .current_dir(&self.config.app_dir)
241 .output()
242 .await
243 .map_err(|e| format!("Failed to execute git reset: {}", e))?;
244
245 if !reset_output.status.success() {
246 return Err(format!(
247 "Git reset failed: {}",
248 String::from_utf8_lossy(&reset_output.stderr)
249 ));
250 }
251
252 info!(" Pulling latest changes...");
253
254 let pull_output = Command::new("git")
255 .arg("pull")
256 .arg("origin")
257 .arg("main")
258 .current_dir(&self.config.app_dir)
259 .output()
260 .await
261 .map_err(|e| format!("Failed to execute git pull: {}", e))?;
262
263 if !pull_output.status.success() {
264 return Err(format!(
265 "Git pull failed: {}",
266 String::from_utf8_lossy(&pull_output.stderr)
267 ));
268 }
269
270 info!(" Git operations completed");
271 Ok(())
272 }
273
274 async fn run_install_command(&self, install_cmd: &str) -> Result<(), String> {
276 info!(" Installing dependencies: {}", install_cmd);
277
278 let install_output = Command::new("sh")
279 .arg("-c")
280 .arg(install_cmd)
281 .envs(&self.config.environment)
282 .current_dir(&self.config.app_dir)
283 .output()
284 .await
285 .map_err(|e| format!("Failed to execute install command: {}", e))?;
286
287 if !install_output.status.success() {
288 return Err(format!(
289 "Install command failed: {}",
290 String::from_utf8_lossy(&install_output.stderr)
291 ));
292 }
293
294 info!(" Dependencies installed successfully");
295 Ok(())
296 }
297
298 async fn run_build_command(&self, build_cmd: &str) -> Result<(), String> {
300 info!(" Building project: {}", build_cmd);
301
302 let build_output = Command::new("sh")
303 .arg("-c")
304 .arg(build_cmd)
305 .envs(&self.config.environment)
306 .current_dir(&self.config.app_dir)
307 .output()
308 .await
309 .map_err(|e| format!("Failed to execute build command: {}", e))?;
310
311 if !build_output.status.success() {
312 return Err(format!(
313 "Build command failed: {}",
314 String::from_utf8_lossy(&build_output.stderr)
315 ));
316 }
317
318 info!("Project built successfully");
319 Ok(())
320 }
321
322 async fn stop_existing_processes(&self) -> Result<(), String> {
324 info!("Stopping existing processes...");
325
326 let pm2_stop = Command::new("pm2")
327 .arg("stop")
328 .arg(&self.config.app_name)
329 .output()
330 .await;
331
332 match pm2_stop {
333 Ok(output) if output.status.success() => {
334 info!(" Stopped PM2 process: {}", self.config.app_name);
335 }
336 _ => {
337 info!(" No existing PM2 process found");
338 }
339 }
340
341 let kill_port = Command::new("sh")
342 .arg("-c")
343 .arg(format!("sudo fuser -k {}/tcp", self.config.port))
344 .output()
345 .await;
346
347 match kill_port {
348 Ok(output) if output.status.success() => {
349 info!(" Killed processes on port {}", self.config.port);
350 }
351 _ => {
352 info!(" No processes found on port {}", self.config.port);
353 }
354 }
355
356 Ok(())
357 }
358
359 async fn start_application(&self) -> Result<(), String> {
361 let start_cmd = self
362 .config
363 .start_command
364 .as_ref()
365 .ok_or("No start command configured")?;
366
367 info!(" Starting application: {}", start_cmd);
368
369 let pm2_cmd = format!("{} --port {}", start_cmd, self.config.port);
371
372 let start_output = Command::new("pm2")
373 .arg("start")
374 .arg(&pm2_cmd)
375 .arg("--name")
376 .arg(&self.config.app_name)
377 .arg("--")
378 .arg("--port")
379 .arg(self.config.port.to_string())
380 .envs(&self.config.environment)
381 .current_dir(&self.config.app_dir)
382 .output()
383 .await
384 .map_err(|e| format!("Failed to start PM2 process: {}", e))?;
385
386 if !start_output.status.success() {
387 return Err(format!(
388 "Failed to start application: {}",
389 String::from_utf8_lossy(&start_output.stderr)
390 ));
391 }
392
393 info!("Application started successfully");
394 Ok(())
395 }
396
397 async fn save_pm2_config(&self) -> Result<(), String> {
399 info!("Saving PM2 configuration...");
400
401 let save_output = Command::new("pm2")
402 .arg("save")
403 .output()
404 .await
405 .map_err(|e| format!("Failed to save PM2 config: {}", e))?;
406
407 if !save_output.status.success() {
408 return Err(format!(
409 "PM2 save failed: {}",
410 String::from_utf8_lossy(&save_output.stderr)
411 ));
412 }
413
414 info!("PM2 configuration saved");
415 Ok(())
416 }
417}