// Workflow handlers use crate::app::App; use crate::models::{ExecutionResultMsg, WorkflowExecution, WorkflowStatus}; use chrono::Local; use std::io; use std::path::{Path, PathBuf}; use std::sync::mpsc; use std::thread; use wrkflw_evaluator::evaluate_workflow_file; use wrkflw_executor::{self, JobStatus, RuntimeType, StepStatus}; // Validate a workflow or directory containing workflows pub fn validate_workflow(path: &Path, verbose: bool) -> io::Result<()> { let mut workflows = Vec::new(); if path.is_dir() { let entries = std::fs::read_dir(path)?; for entry in entries { let entry = entry?; let entry_path = entry.path(); if entry_path.is_file() && wrkflw_utils::is_workflow_file(&entry_path) { workflows.push(entry_path); } } } else if path.is_file() { workflows.push(PathBuf::from(path)); } else { return Err(io::Error::new( io::ErrorKind::NotFound, format!("Path does not exist: {}", path.display()), )); } let mut valid_count = 0; let mut invalid_count = 0; println!("Validating {} workflow file(s)...", workflows.len()); for workflow_path in workflows { match evaluate_workflow_file(&workflow_path, verbose) { Ok(result) => { if result.is_valid { println!("✅ Valid: {}", workflow_path.display()); valid_count += 1; } else { println!("❌ Invalid: {}", workflow_path.display()); for (i, issue) in result.issues.iter().enumerate() { println!(" {}. {}", i + 1, issue); } invalid_count += 1; } } Err(e) => { println!("❌ Error processing {}: {}", workflow_path.display(), e); invalid_count += 1; } } } println!( "\nSummary: {} valid, {} invalid", valid_count, invalid_count ); Ok(()) } // Execute a workflow through the CLI pub async fn execute_workflow_cli( path: &Path, runtime_type: RuntimeType, verbose: bool, ) -> io::Result<()> { if !path.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, format!("Workflow file does not exist: {}", path.display()), )); } println!("Validating workflow..."); match evaluate_workflow_file(path, false) { Ok(result) => { if !result.is_valid { println!("❌ Cannot execute invalid workflow: {}", path.display()); for (i, issue) in result.issues.iter().enumerate() { println!(" {}. {}", i + 1, issue); } return Err(io::Error::new( io::ErrorKind::InvalidData, "Workflow validation failed", )); } } Err(e) => { return Err(io::Error::other(format!( "Error validating workflow: {}", e ))); } } // Check container runtime availability if container runtime is selected let runtime_type = match runtime_type { RuntimeType::Docker => { if !wrkflw_executor::docker::is_available() { println!("⚠️ Docker is not available. Using emulation mode instead."); wrkflw_logging::warning("Docker is not available. Using emulation mode instead."); RuntimeType::Emulation } else { RuntimeType::Docker } } RuntimeType::Podman => { if !wrkflw_executor::podman::is_available() { println!("⚠️ Podman is not available. Using emulation mode instead."); wrkflw_logging::warning("Podman is not available. Using emulation mode instead."); RuntimeType::Emulation } else { RuntimeType::Podman } } RuntimeType::SecureEmulation => RuntimeType::SecureEmulation, RuntimeType::Emulation => RuntimeType::Emulation, }; println!("Executing workflow: {}", path.display()); println!("Runtime mode: {:?}", runtime_type); // Log the start of the execution in debug mode with more details wrkflw_logging::debug(&format!( "Starting workflow execution: path={}, runtime={:?}, verbose={}", path.display(), runtime_type, verbose )); let config = wrkflw_executor::ExecutionConfig { runtime_type, verbose, preserve_containers_on_failure: false, // Default for this path }; match wrkflw_executor::execute_workflow(path, config).await { Ok(result) => { println!("\nWorkflow execution results:"); // Track if the workflow had any failures let mut any_job_failed = false; for job in &result.jobs { match job.status { JobStatus::Success => { println!("\n✅ Job succeeded: {}", job.name); } JobStatus::Failure => { println!("\n❌ Job failed: {}", job.name); any_job_failed = true; } JobStatus::Skipped => { println!("\n⏭️ Job skipped: {}", job.name); } } println!("-------------------------"); // Log the job details for debug purposes wrkflw_logging::debug(&format!("Job: {}, Status: {:?}", job.name, job.status)); for step in job.steps.iter() { match step.status { StepStatus::Success => { println!(" ✅ {}", step.name); // Check if this is a GitHub action output that should be hidden let should_hide = std::env::var("WRKFLW_HIDE_ACTION_MESSAGES") .map(|val| val == "true") .unwrap_or(false) && step.output.contains("Would execute GitHub action:"); // Only show output if not hidden and it's short if !should_hide && !step.output.trim().is_empty() && step.output.lines().count() <= 3 { // For short outputs, show directly println!(" {}", step.output.trim()); } } StepStatus::Failure => { println!(" ❌ {}", step.name); // Ensure we capture and show exit code if let Some(exit_code) = step .output .lines() .find(|line| line.trim().starts_with("Exit code:")) .map(|line| line.trim().to_string()) { println!(" {}", exit_code); } // Show command/run details in debug mode if wrkflw_logging::get_log_level() <= wrkflw_logging::LogLevel::Debug { if let Some(cmd_output) = step .output .lines() .skip_while(|l| !l.trim().starts_with("$")) .take(1) .next() { println!(" Command: {}", cmd_output.trim()); } } // Always show error output from failed steps, but keep it to a reasonable length let output_lines: Vec<&str> = step .output .lines() .filter(|line| !line.trim().starts_with("Exit code:")) .collect(); if !output_lines.is_empty() { println!(" Error output:"); for line in output_lines.iter().take(10) { println!(" {}", line.trim().replace('\n', "\n ")); } if output_lines.len() > 10 { println!( " ... (and {} more lines)", output_lines.len() - 10 ); println!(" Use --debug to see full output"); } } } StepStatus::Skipped => { println!(" ⏭️ {} (skipped)", step.name); } } // Always log the step details for debug purposes wrkflw_logging::debug(&format!( "Step: {}, Status: {:?}, Output length: {} lines", step.name, step.status, step.output.lines().count() )); // In debug mode, log all step output if wrkflw_logging::get_log_level() == wrkflw_logging::LogLevel::Debug && !step.output.trim().is_empty() { wrkflw_logging::debug(&format!( "Step output for '{}': \n{}", step.name, step.output )); } } } if any_job_failed { println!("\n❌ Workflow completed with failures"); // In the case of failure, we'll also inform the user about the debug option // if they're not already using it if wrkflw_logging::get_log_level() > wrkflw_logging::LogLevel::Debug { println!(" Run with --debug for more detailed output"); } } else { println!("\n✅ Workflow completed successfully!"); } Ok(()) } Err(e) => { println!("❌ Failed to execute workflow: {}", e); wrkflw_logging::error(&format!("Failed to execute workflow: {}", e)); Err(io::Error::other(e)) } } } // Helper function to execute workflow trigger using curl pub async fn execute_curl_trigger( workflow_name: &str, branch: Option<&str>, ) -> Result<(Vec, ()), String> { // Get GitHub token let token = std::env::var("GITHUB_TOKEN").map_err(|_| { "GitHub token not found. Please set GITHUB_TOKEN environment variable".to_string() })?; // Debug log to check if GITHUB_TOKEN is set match std::env::var("GITHUB_TOKEN") { Ok(token) => wrkflw_logging::info(&format!("GITHUB_TOKEN is set: {}", &token[..5])), // Log first 5 characters for security Err(_) => wrkflw_logging::error("GITHUB_TOKEN is not set"), } // Get repository information let repo_info = wrkflw_github::get_repo_info() .map_err(|e| format!("Failed to get repository info: {}", e))?; // Determine branch to use let branch_ref = branch.unwrap_or(&repo_info.default_branch); // Extract just the workflow name from the path if it's a full path let workflow_name = if workflow_name.contains('/') { Path::new(workflow_name) .file_stem() .and_then(|s| s.to_str()) .ok_or_else(|| "Invalid workflow name".to_string())? } else { workflow_name }; wrkflw_logging::info(&format!("Using workflow name: {}", workflow_name)); // Construct JSON payload let payload = serde_json::json!({ "ref": branch_ref }); // Construct API URL let url = format!( "https://api.github.com/repos/{}/{}/actions/workflows/{}.yml/dispatches", repo_info.owner, repo_info.repo, workflow_name ); wrkflw_logging::info(&format!("Triggering workflow at URL: {}", url)); // Create a reqwest client let client = reqwest::Client::new(); // Send the request using reqwest let response = client .post(&url) .header("Authorization", format!("Bearer {}", token.trim())) .header("Accept", "application/vnd.github.v3+json") .header("Content-Type", "application/json") .header("User-Agent", "wrkflw-cli") .json(&payload) .send() .await .map_err(|e| format!("Failed to send request: {}", e))?; if !response.status().is_success() { let status = response.status().as_u16(); let error_message = response .text() .await .unwrap_or_else(|_| format!("Unknown error (HTTP {})", status)); return Err(format!("API error: {} - {}", status, error_message)); } // Success message with URL to view the workflow let success_msg = format!( "Workflow triggered successfully. View it at: https://github.com/{}/{}/actions/workflows/{}.yml", repo_info.owner, repo_info.repo, workflow_name ); // Create a job result structure let job_result = wrkflw_executor::JobResult { name: "GitHub Trigger".to_string(), status: wrkflw_executor::JobStatus::Success, steps: vec![wrkflw_executor::StepResult { name: "Remote Trigger".to_string(), status: wrkflw_executor::StepStatus::Success, output: success_msg, }], logs: "Workflow triggered remotely on GitHub".to_string(), }; Ok((vec![job_result], ())) } // Extract common workflow execution logic to avoid duplication pub fn start_next_workflow_execution( app: &mut App, tx_clone: &mpsc::Sender, verbose: bool, ) { if let Some(next_idx) = app.get_next_workflow_to_execute() { app.current_execution = Some(next_idx); let tx_clone_inner = tx_clone.clone(); let workflow_path = app.workflows[next_idx].path.clone(); // Log whether verbose mode is enabled if verbose { app.logs .push("Verbose mode: Step outputs will be displayed in full".to_string()); wrkflw_logging::info("Verbose mode: Step outputs will be displayed in full"); } else { app.logs.push( "Standard mode: Only step status will be shown (use --verbose for full output)" .to_string(), ); wrkflw_logging::info( "Standard mode: Only step status will be shown (use --verbose for full output)", ); } // Check container runtime availability again if container runtime is selected let runtime_type = match app.runtime_type { RuntimeType::Docker => { // Use safe FD redirection to check Docker availability let is_docker_available = match wrkflw_utils::fd::with_stderr_to_null( wrkflw_executor::docker::is_available, ) { Ok(result) => result, Err(_) => { wrkflw_logging::debug( "Failed to redirect stderr when checking Docker availability.", ); false } }; if !is_docker_available { app.logs .push("Docker is not available. Using emulation mode instead.".to_string()); wrkflw_logging::warning( "Docker is not available. Using emulation mode instead.", ); RuntimeType::Emulation } else { RuntimeType::Docker } } RuntimeType::Podman => { // Use safe FD redirection to check Podman availability let is_podman_available = match wrkflw_utils::fd::with_stderr_to_null( wrkflw_executor::podman::is_available, ) { Ok(result) => result, Err(_) => { wrkflw_logging::debug( "Failed to redirect stderr when checking Podman availability.", ); false } }; if !is_podman_available { app.logs .push("Podman is not available. Using emulation mode instead.".to_string()); wrkflw_logging::warning( "Podman is not available. Using emulation mode instead.", ); RuntimeType::Emulation } else { RuntimeType::Podman } } RuntimeType::SecureEmulation => RuntimeType::SecureEmulation, RuntimeType::Emulation => RuntimeType::Emulation, }; let validation_mode = app.validation_mode; let preserve_containers_on_failure = app.preserve_containers_on_failure; // Update workflow status and add execution details app.workflows[next_idx].status = WorkflowStatus::Running; // Initialize execution details if not already done if app.workflows[next_idx].execution_details.is_none() { app.workflows[next_idx].execution_details = Some(WorkflowExecution { jobs: Vec::new(), start_time: Local::now(), end_time: None, logs: Vec::new(), progress: 0.0, }); } thread::spawn(move || { let rt = match tokio::runtime::Runtime::new() { Ok(runtime) => runtime, Err(e) => { let _ = tx_clone_inner.send(( next_idx, Err(format!("Failed to create Tokio runtime: {}", e)), )); return; } }; let result = rt.block_on(async { if validation_mode { // Perform validation instead of execution match evaluate_workflow_file(&workflow_path, verbose) { Ok(validation_result) => { // Create execution result based on validation let status = if validation_result.is_valid { wrkflw_executor::JobStatus::Success } else { wrkflw_executor::JobStatus::Failure }; // Create a synthetic job result for validation let jobs = vec![wrkflw_executor::JobResult { name: "Validation".to_string(), status, steps: vec![wrkflw_executor::StepResult { name: "Validator".to_string(), status: if validation_result.is_valid { wrkflw_executor::StepStatus::Success } else { wrkflw_executor::StepStatus::Failure }, output: validation_result.issues.join("\n"), }], logs: format!( "Validation result: {}", if validation_result.is_valid { "PASSED" } else { "FAILED" } ), }]; Ok((jobs, ())) } Err(e) => Err(e.to_string()), } } else { // Use safe FD redirection for execution let config = wrkflw_executor::ExecutionConfig { runtime_type, verbose, preserve_containers_on_failure, }; let execution_result = wrkflw_utils::fd::with_stderr_to_null(|| { futures::executor::block_on(async { wrkflw_executor::execute_workflow(&workflow_path, config).await }) }) .map_err(|e| format!("Failed to redirect stderr during execution: {}", e))?; match execution_result { Ok(execution_result) => { // Send back the job results in a wrapped result Ok((execution_result.jobs, ())) } Err(e) => Err(e.to_string()), } } }); // Only send if we get a valid result if let Err(e) = tx_clone_inner.send((next_idx, result)) { wrkflw_logging::error(&format!("Error sending execution result: {}", e)); } }); } else { app.running = false; let timestamp = Local::now().format("%H:%M:%S").to_string(); app.logs .push(format!("[{}] All workflows completed execution", timestamp)); wrkflw_logging::info("All workflows completed execution"); } }