diff --git a/Cargo.lock b/Cargo.lock index 8be13da..8779f79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -798,6 +798,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1172,6 +1181,12 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.3" @@ -1721,6 +1736,7 @@ name = "runtime" version = "0.4.0" dependencies = [ "async-trait", + "futures", "logging", "models", "once_cell", @@ -1728,6 +1744,8 @@ dependencies = [ "serde_yaml", "tempfile", "tokio", + "utils", + "which", ] [[package]] @@ -1736,6 +1754,19 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.3" @@ -1745,7 +1776,7 @@ dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.3", "windows-sys 0.59.0", ] @@ -2064,7 +2095,7 @@ dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", - "rustix", + "rustix 1.0.3", "windows-sys 0.59.0", ] @@ -2449,6 +2480,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2728,7 +2771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix", + "rustix 1.0.3", ] [[package]] diff --git a/crates/executor/src/engine.rs b/crates/executor/src/engine.rs index 89072f5..d752128 100644 --- a/crates/executor/src/engine.rs +++ b/crates/executor/src/engine.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] use bollard::Docker; use futures::future; use regex; @@ -269,6 +270,12 @@ fn create_gitlab_context(pipeline: &Pipeline, workspace_dir: &Path) -> HashMap) -> Result Some(client), - Err(e) => { - logging::error(&format!("Failed to connect to Docker: {}", e)); - None - } - } - } else { - None - }; - - // Create a Docker network for this job if we have services - let network_id = if !job.services.is_empty() && docker_client.is_some() { - let docker = match docker_client.as_ref() { - Some(client) => client, - None => { - return Err(ExecutionError::Runtime( - "Docker client is required but not available".to_string(), - )); - } - }; - match docker::create_job_network(docker).await { - Ok(id) => { - logging::info(&format!( - "Created network {} for job '{}'", - id, ctx.job_name - )); - Some(id) - } - Err(e) => { - logging::error(&format!( - "Failed to create network for job '{}': {}", - ctx.job_name, e - )); - return Err(ExecutionError::Runtime(format!( - "Failed to create network: {}", - e - ))); - } - } - } else { - None - }; - - // Start service containers if any - let mut service_containers = Vec::new(); - - if !job.services.is_empty() { - if docker_client.is_none() { - logging::error("Services are only supported with Docker runtime"); - return Err(ExecutionError::Runtime( - "Services require Docker runtime".to_string(), - )); - } - - logging::info(&format!( - "Starting {} service containers for job '{}'", - job.services.len(), - ctx.job_name - )); - - let docker = match docker_client.as_ref() { - Some(client) => client, - None => { - return Err(ExecutionError::Runtime( - "Docker client is required but not available".to_string(), - )); - } - }; - - #[allow(unused_variables, unused_assignments)] - for (service_name, service_config) in &job.services { - logging::info(&format!( - "Starting service '{}' with image '{}'", - service_name, service_config.image - )); - - // Prepare container configuration - let container_name = format!("wrkflw-service-{}-{}", ctx.job_name, service_name); - - // Map ports if specified - let mut port_bindings = HashMap::new(); - if let Some(ports) = &service_config.ports { - for port_spec in ports { - // Parse port spec like "8080:80" - let parts: Vec<&str> = port_spec.split(':').collect(); - if parts.len() == 2 { - let host_port = parts[0]; - let container_port = parts[1]; - - let port_binding = bollard::models::PortBinding { - host_ip: Some("0.0.0.0".to_string()), - host_port: Some(host_port.to_string()), - }; - - let key = format!("{}/tcp", container_port); - port_bindings.insert(key, Some(vec![port_binding])); - } - } - } - - // Convert environment variables - let env_vars: Vec = service_config - .env - .iter() - .map(|(k, v)| format!("{}={}", k, v)) - .collect(); - - // Create container options - let create_opts = bollard::container::CreateContainerOptions { - name: container_name, - platform: None, - }; - - // Host configuration - let host_config = bollard::models::HostConfig { - port_bindings: Some(port_bindings), - network_mode: network_id.clone(), - ..Default::default() - }; - - // Container configuration - let config = bollard::container::Config { - image: Some(service_config.image.clone()), - env: Some(env_vars), - host_config: Some(host_config), - ..Default::default() - }; - - // Log the network connection - if network_id.is_some() { - logging::info(&format!( - "Service '{}' connected to network via host_config", - service_name - )); - } - - match docker.create_container(Some(create_opts), config).await { - Ok(response) => { - let container_id = response.id; - - // Track the container for cleanup - docker::track_container(&container_id); - service_containers.push(container_id.clone()); - - // Start the container - match docker.start_container::(&container_id, None).await { - Ok(_) => { - logging::info(&format!("Started service container: {}", container_id)); - - // Add service address to environment - job_env.insert( - format!("{}_HOST", service_name.to_uppercase()), - service_name.clone(), - ); - - job_logs.push_str(&format!( - "Started service '{}' with container ID: {}\n", - service_name, container_id - )); - } - Err(e) => { - let error_msg = format!( - "Failed to start service container '{}': {}", - service_name, e - ); - logging::error(&error_msg); - - // Clean up the created container - let _ = docker.remove_container(&container_id, None).await; - - // Clean up network if created - if let Some(net_id) = &network_id { - let _ = docker.remove_network(net_id).await; - docker::untrack_network(net_id); - } - - return Err(ExecutionError::Runtime(error_msg)); - } - } - } - Err(e) => { - let error_msg = format!( - "Failed to create service container '{}': {}", - service_name, e - ); - logging::error(&error_msg); - - // Clean up network if created - if let Some(net_id) = &network_id { - let _ = docker.remove_network(net_id).await; - docker::untrack_network(net_id); - } - - return Err(ExecutionError::Runtime(error_msg)); - } - } - } - - // Give services a moment to start up - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - } - - // Prepare the runner environment - let runner_image = get_runner_image(&job.runs_on); - prepare_runner_image(&runner_image, ctx.runtime, ctx.verbose).await?; - - // Copy project files to workspace + // Get the current project directory let current_dir = std::env::current_dir().map_err(|e| { ExecutionError::Execution(format!("Failed to get current directory: {}", e)) })?; + + // Copy project files to the job workspace directory + logging::info(&format!( + "Copying project files to job workspace: {}", + job_dir.path().display() + )); copy_directory_contents(¤t_dir, job_dir.path())?; logging::info(&format!("Executing job: {}", ctx.job_name)); @@ -840,7 +644,7 @@ async fn execute_job(ctx: JobExecutionContext<'_>) -> Result) -> Result client, - None => { - return Err(ExecutionError::Runtime( - "Docker client is required but not available".to_string(), - )); - } - }; - - for container_id in &service_containers { - logging::info(&format!("Stopping service container: {}", container_id)); - - let _ = docker.stop_container(container_id, None).await; - let _ = docker.remove_container(container_id, None).await; - - // Untrack container since we've explicitly removed it - docker::untrack_container(container_id); - } - } - - // Clean up network if created - if let Some(net_id) = &network_id { - if docker_client.is_some() { - let docker = match docker_client.as_ref() { - Some(client) => client, - None => { - return Err(ExecutionError::Runtime( - "Docker client is required but not available".to_string(), - )); - } - }; - - logging::info(&format!("Removing network: {}", net_id)); - if let Err(e) = docker.remove_network(net_id).await { - logging::error(&format!("Failed to remove network {}: {}", net_id, e)); - } - - // Untrack network since we've explicitly removed it - docker::untrack_network(net_id); - } - } - Ok(JobResult { name: ctx.job_name.to_string(), status: if job_success { @@ -1055,14 +815,16 @@ async fn execute_matrix_job( let job_dir = tempfile::tempdir() .map_err(|e| ExecutionError::Execution(format!("Failed to create job directory: {}", e)))?; - // Prepare the runner - let runner_image = get_runner_image(&job_template.runs_on); - prepare_runner_image(&runner_image, runtime, verbose).await?; - - // Copy project files to workspace + // Get the current project directory let current_dir = std::env::current_dir().map_err(|e| { ExecutionError::Execution(format!("Failed to get current directory: {}", e)) })?; + + // Copy project files to the job workspace directory + logging::info(&format!( + "Copying project files to job workspace: {}", + job_dir.path().display() + )); copy_directory_contents(¤t_dir, job_dir.path())?; let job_success = if job_template.steps.is_empty() { @@ -1078,7 +840,7 @@ async fn execute_matrix_job( working_dir: job_dir.path(), runtime, workflow, - runner_image: &runner_image, + runner_image: &get_runner_image(&job_template.runs_on), verbose, matrix_combination: &Some(combination.values.clone()), }) @@ -1918,6 +1680,7 @@ fn get_runner_image(runs_on: &str) -> String { .to_string() } +#[allow(dead_code)] async fn prepare_runner_image( image: &str, runtime: &dyn ContainerRuntime, @@ -1947,7 +1710,7 @@ async fn prepare_runner_image( Ok(()) } -/// Extract language and version information from an image name +#[allow(dead_code)] fn extract_language_info(image: &str) -> Option<(&'static str, Option<&str>)> { let image_lower = image.to_lowercase(); diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 707b19b..77772e7 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -8,12 +8,15 @@ license.workspace = true [dependencies] # Internal crates models = { path = "../models" } -logging = { path = "../logging" } +logging = { path = "../logging", version = "0.4.0" } # External dependencies async-trait.workspace = true -once_cell.workspace = true +once_cell = "1.19" serde.workspace = true serde_yaml.workspace = true -tempfile.workspace = true +tempfile = "3.9" tokio.workspace = true +futures = "0.3" +utils = { path = "../utils", version = "0.4.0" } +which = "4.4" diff --git a/crates/runtime/src/emulation.rs b/crates/runtime/src/emulation.rs index 9b03167..c10144b 100644 --- a/crates/runtime/src/emulation.rs +++ b/crates/runtime/src/emulation.rs @@ -8,6 +8,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Mutex; use tempfile::TempDir; +use which; // Global collection of resources to clean up static EMULATION_WORKSPACES: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); @@ -160,29 +161,189 @@ impl ContainerRuntime for EmulationRuntime { command_str.push_str(part); } - // Log the command being executed + // Log more detailed debugging information logging::info(&format!("Executing command in container: {}", command_str)); + logging::info(&format!("Working directory: {}", working_dir.display())); + logging::info(&format!("Command length: {}", command.len())); - // Special handling for Rust/Cargo actions - if command_str.contains("rust") || command_str.contains("cargo") { - logging::debug(&format!("Executing Rust command: {}", command_str)); + if command.is_empty() { + return Err(ContainerError::ContainerExecution( + "Empty command array".to_string(), + )); + } - let mut cmd = Command::new("cargo"); - let parts = command_str.split_whitespace().collect::>(); + // Print each command part separately for debugging + for (i, part) in command.iter().enumerate() { + logging::info(&format!("Command part {}: '{}'", i, part)); + } - let current_dir = working_dir.to_str().unwrap_or("."); - cmd.current_dir(current_dir); + // Log environment variables + logging::info("Environment variables:"); + for (key, value) in env_vars { + logging::info(&format!(" {}={}", key, value)); + } + + // Find actual working directory - determine if we should use the current directory instead + let actual_working_dir: PathBuf = if !working_dir.exists() { + // Look for GITHUB_WORKSPACE or CI_PROJECT_DIR in env_vars + let mut workspace_path = None; + for (key, value) in env_vars { + if *key == "GITHUB_WORKSPACE" || *key == "CI_PROJECT_DIR" { + workspace_path = Some(PathBuf::from(value)); + break; + } + } + + // If found, use that as the working directory + if let Some(path) = workspace_path { + if path.exists() { + logging::info(&format!( + "Using environment-defined workspace: {}", + path.display() + )); + path + } else { + // Fallback to current directory + let current_dir = + std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + logging::info(&format!( + "Using current directory: {}", + current_dir.display() + )); + current_dir + } + } else { + // Fallback to current directory + let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + logging::info(&format!( + "Using current directory: {}", + current_dir.display() + )); + current_dir + } + } else { + working_dir.to_path_buf() + }; + + logging::info(&format!( + "Using actual working directory: {}", + actual_working_dir.display() + )); + + // Check if path contains the command (for shell script execution) + let command_path = which::which(command[0]); + match &command_path { + Ok(path) => logging::info(&format!("Found command at: {}", path.display())), + Err(e) => logging::error(&format!( + "Command not found in PATH: {} - Error: {}", + command[0], e + )), + } + + // First, check if this is a simple shell command (like echo) + if command_str.starts_with("echo ") + || command_str.starts_with("cp ") + || command_str.starts_with("mkdir ") + || command_str.starts_with("mv ") + { + logging::info("Executing as shell command"); + // Execute as a shell command + let mut cmd = Command::new("sh"); + cmd.arg("-c"); + cmd.arg(&command_str); + cmd.current_dir(&actual_working_dir); // Add environment variables for (key, value) in env_vars { cmd.env(key, value); } + match cmd.output() { + Ok(output_result) => { + let exit_code = output_result.status.code().unwrap_or(-1); + let output = String::from_utf8_lossy(&output_result.stdout).to_string(); + let error = String::from_utf8_lossy(&output_result.stderr).to_string(); + + logging::debug(&format!( + "Shell command completed with exit code: {}", + exit_code + )); + + if exit_code != 0 { + let mut error_details = format!( + "Command failed with exit code: {}\nCommand: {}\n\nError output:\n{}", + exit_code, command_str, error + ); + + // Add environment variables to error details + error_details.push_str("\n\nEnvironment variables:\n"); + for (key, value) in env_vars { + if key.starts_with("GITHUB_") || key.starts_with("CI_") { + error_details.push_str(&format!("{}={}\n", key, value)); + } + } + + return Err(ContainerError::ContainerExecution(error_details)); + } + + return Ok(ContainerOutput { + stdout: output, + stderr: error, + exit_code, + }); + } + Err(e) => { + return Err(ContainerError::ContainerExecution(format!( + "Failed to execute command: {}\nError: {}", + command_str, e + ))); + } + } + } + + // Special handling for Rust/Cargo commands + if command_str.starts_with("cargo ") || command_str.starts_with("rustup ") { + let parts: Vec<&str> = command_str.split_whitespace().collect(); + if parts.is_empty() { + return Err(ContainerError::ContainerExecution( + "Empty command".to_string(), + )); + } + + let mut cmd = Command::new(parts[0]); + + // Always use the current directory for cargo/rust commands rather than the temporary directory + let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + logging::info(&format!( + "Using project directory for Rust command: {}", + current_dir.display() + )); + cmd.current_dir(¤t_dir); + + // Add environment variables + for (key, value) in env_vars { + // Don't use the CI_PROJECT_DIR for CARGO_HOME, use the actual project directory + if *key == "CARGO_HOME" && value.contains("${CI_PROJECT_DIR}") { + let cargo_home = + value.replace("${CI_PROJECT_DIR}", ¤t_dir.to_string_lossy()); + logging::info(&format!("Setting CARGO_HOME to: {}", cargo_home)); + cmd.env(key, cargo_home); + } else { + cmd.env(key, value); + } + } + // Add command arguments if parts.len() > 1 { cmd.args(&parts[1..]); } + logging::debug(&format!( + "Executing Rust command: {} in {}", + command_str, + current_dir.display() + )); + match cmd.output() { Ok(output_result) => { let exit_code = output_result.status.code().unwrap_or(-1); @@ -200,7 +361,11 @@ impl ContainerRuntime for EmulationRuntime { // Add environment variables to error details error_details.push_str("\n\nEnvironment variables:\n"); for (key, value) in env_vars { - if key.starts_with("GITHUB_") || key.starts_with("RUST") { + if key.starts_with("GITHUB_") + || key.starts_with("RUST") + || key.starts_with("CARGO") + || key.starts_with("CI_") + { error_details.push_str(&format!("{}={}\n", key, value)); } } @@ -223,11 +388,11 @@ impl ContainerRuntime for EmulationRuntime { } } - // For other commands, use a shell + // For other commands, use a shell as fallback let mut cmd = Command::new("sh"); cmd.arg("-c"); cmd.arg(&command_str); - cmd.current_dir(working_dir.to_str().unwrap_or(".")); + cmd.current_dir(&actual_working_dir); // Add environment variables for (key, value) in env_vars { @@ -251,7 +416,7 @@ impl ContainerRuntime for EmulationRuntime { // Add environment variables to error details error_details.push_str("\n\nEnvironment variables:\n"); for (key, value) in env_vars { - if key.starts_with("GITHUB_") { + if key.starts_with("GITHUB_") || key.starts_with("CI_") { error_details.push_str(&format!("{}={}\n", key, value)); } } diff --git a/crates/wrkflw/src/main.rs b/crates/wrkflw/src/main.rs index 3651b0d..b9b4a90 100644 --- a/crates/wrkflw/src/main.rs +++ b/crates/wrkflw/src/main.rs @@ -336,14 +336,32 @@ async fn main() { if result.failure_details.is_some() { eprintln!("❌ Workflow execution failed:"); if let Some(details) = result.failure_details { - eprintln!("{}", details); + if verbose { + // Show full error details in verbose mode + eprintln!("{}", details); + } else { + // Show simplified error info in non-verbose mode + let simplified_error = details + .lines() + .filter(|line| line.contains("❌") || line.trim().starts_with("Error:")) + .take(5) // Limit to the first 5 error lines + .collect::>() + .join("\n"); + + eprintln!("{}", simplified_error); + + if details.lines().count() > 5 { + eprintln!("\nUse --verbose flag to see full error details"); + } + } } std::process::exit(1); } else { println!("✅ Workflow execution completed successfully!"); // Print a summary of executed jobs - if verbose { + if true { + // Always show job summary println!("\nJob summary:"); for job in result.jobs { println!( @@ -361,18 +379,42 @@ async fn main() { } ); - if debug { - println!(" Steps:"); - for step in job.steps { - println!( - " {} {}", - match step.status { - executor::StepStatus::Success => "✅", - executor::StepStatus::Failure => "❌", - executor::StepStatus::Skipped => "⏭️", - }, - step.name - ); + // Always show steps, not just in debug mode + println!(" Steps:"); + for step in job.steps { + let step_status = match step.status { + executor::StepStatus::Success => "✅", + executor::StepStatus::Failure => "❌", + executor::StepStatus::Skipped => "⏭️", + }; + + println!(" {} {}", step_status, step.name); + + // If step failed and we're not in verbose mode, show condensed error info + if step.status == executor::StepStatus::Failure && !verbose { + // Extract error information from step output + let error_lines = step + .output + .lines() + .filter(|line| { + line.contains("error:") + || line.contains("Error:") + || line.trim().starts_with("Exit code:") + || line.contains("failed") + }) + .take(3) // Limit to 3 most relevant error lines + .collect::>(); + + if !error_lines.is_empty() { + println!(" Error details:"); + for line in error_lines { + println!(" {}", line.trim()); + } + + if step.output.lines().count() > 3 { + println!(" (Use --verbose for full output)"); + } + } } } }