fix: fixed the issues in viewing step details in non verbose mode

This commit is contained in:
bahdotsh
2025-05-02 15:45:51 +05:30
parent a97398f949
commit 00fa569add
5 changed files with 310 additions and 294 deletions

49
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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<S
workspace_dir.to_string_lossy().to_string(),
);
// Also add the workspace as the GitHub workspace for compatibility with emulation runtime
env_context.insert(
"GITHUB_WORKSPACE".to_string(),
workspace_dir.to_string_lossy().to_string(),
);
// Add global variables from the pipeline
if let Some(variables) = &pipeline.variables {
for (key, value) in variables {
@@ -612,219 +619,16 @@ async fn execute_job(ctx: JobExecutionContext<'_>) -> Result<JobResult, Executio
let job_dir = tempfile::tempdir()
.map_err(|e| ExecutionError::Execution(format!("Failed to create job directory: {}", e)))?;
// Try to get a Docker client if using Docker and services exist
let docker_client = if !job.services.is_empty() {
match Docker::connect_with_local_defaults() {
Ok(client) => 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<String> = 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::<String>(&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(&current_dir, job_dir.path())?;
logging::info(&format!("Executing job: {}", ctx.job_name));
@@ -840,7 +644,7 @@ async fn execute_job(ctx: JobExecutionContext<'_>) -> Result<JobResult, Executio
working_dir: job_dir.path(),
runtime: ctx.runtime,
workflow: ctx.workflow,
runner_image: &runner_image,
runner_image: &get_runner_image(&job.runs_on),
verbose: ctx.verbose,
matrix_combination: &None,
})
@@ -889,50 +693,6 @@ async fn execute_job(ctx: JobExecutionContext<'_>) -> Result<JobResult, Executio
}
}
// Clean up service containers
if !service_containers.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(),
));
}
};
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(&current_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();

View File

@@ -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"

View File

@@ -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<Mutex<Vec<PathBuf>>> = 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::<Vec<&str>>();
// 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(&current_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}", &current_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));
}
}

View File

@@ -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::<Vec<&str>>()
.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::<Vec<&str>>();
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)");
}
}
}
}
}