diff --git a/README.md b/README.md index da17a8c..6dcb428 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ WRKFLW is a powerful command-line tool for validating and executing GitHub Actio ## Features - **TUI Interface**: A full-featured terminal user interface for managing and monitoring workflow executions -- **Validate Workflow Files**: Check for syntax errors and common mistakes in GitHub Actions workflow files +- **Validate Workflow Files**: Check for syntax errors and common mistakes in GitHub Actions workflow files with proper exit codes for CI/CD integration - **Execute Workflows Locally**: Run workflows directly on your machine using Docker containers - **Emulation Mode**: Optional execution without Docker by emulating the container environment locally - **Job Dependency Resolution**: Automatically determines the correct execution order based on job dependencies @@ -77,8 +77,38 @@ wrkflw validate path/to/workflows # Validate with verbose output wrkflw validate --verbose path/to/workflow.yml + +# Validate GitLab CI pipelines +wrkflw validate .gitlab-ci.yml --gitlab + +# Disable exit codes for custom error handling (default: enabled) +wrkflw validate --no-exit-code path/to/workflow.yml ``` +#### Exit Codes for CI/CD Integration + +By default, `wrkflw validate` sets the exit code to `1` when validation fails, making it perfect for CI/CD pipelines and scripts: + +```bash +# In CI/CD scripts - validation failure will cause the script to exit +if ! wrkflw validate; then + echo "❌ Workflow validation failed!" + exit 1 +fi +echo "✅ All workflows are valid!" + +# For custom error handling, disable exit codes +wrkflw validate --no-exit-code +if [ $? -eq 0 ]; then + echo "Validation completed (check output for details)" +fi +``` + +**Exit Code Behavior:** +- `0`: All validations passed successfully +- `1`: One or more validation failures detected +- `2`: Command usage error (invalid arguments, file not found, etc.) + ### Running Workflows in CLI Mode ```bash @@ -143,17 +173,25 @@ The terminal user interface provides an interactive way to manage workflows: ```bash $ wrkflw validate .github/workflows/rust.yml +Validating GitHub workflow file: .github/workflows/rust.yml... Validating 1 workflow file(s)... +✅ Valid: .github/workflows/rust.yml -Validating workflows in: .github/workflows/rust.yml -============================================================ -✅ Valid: rust.yml ------------------------------------------------------------- +Summary: 1 valid, 0 invalid -Summary -============================================================ -✅ 1 valid workflow file(s) +$ echo $? +0 -All workflows are valid! 🎉 +# Example with validation failure +$ wrkflw validate .github/workflows/invalid.yml +Validating GitHub workflow file: .github/workflows/invalid.yml... Validating 1 workflow file(s)... +❌ Invalid: .github/workflows/invalid.yml + 1. Job 'test' is missing 'runs-on' field + 2. Job 'test' is missing 'steps' section + +Summary: 0 valid, 1 invalid + +$ echo $? +1 ``` ### Running a Workflow @@ -246,7 +284,7 @@ This allows you to inspect the exact state of the container when the failure occ ## Limitations ### Supported Features -- ✅ Basic workflow syntax and validation (all YAML syntax checks, required fields, and structure) +- ✅ Basic workflow syntax and validation (all YAML syntax checks, required fields, and structure) with proper exit codes for CI/CD integration - ✅ Job dependency resolution and parallel execution (all jobs with correct 'needs' relationships are executed in the right order, and independent jobs run in parallel) - ✅ Matrix builds (supported for reasonable matrix sizes; very large matrices may be slow or resource-intensive) - ✅ Environment variables and GitHub context (all standard GitHub Actions environment variables and context objects are emulated) diff --git a/crates/wrkflw/src/main.rs b/crates/wrkflw/src/main.rs index dbbe312..19b4f76 100644 --- a/crates/wrkflw/src/main.rs +++ b/crates/wrkflw/src/main.rs @@ -34,6 +34,14 @@ enum Commands { /// Explicitly validate as GitLab CI/CD pipeline #[arg(long)] gitlab: bool, + + /// Set exit code to 1 on validation failure + #[arg(long = "exit-code", default_value_t = true)] + exit_code: bool, + + /// Don't set exit code to 1 on validation failure (overrides --exit-code) + #[arg(long = "no-exit-code", conflicts_with = "exit_code")] + no_exit_code: bool, }, /// Execute workflow or pipeline files locally @@ -257,7 +265,12 @@ async fn main() { tokio::spawn(handle_signals()); match &cli.command { - Some(Commands::Validate { path, gitlab }) => { + Some(Commands::Validate { + path, + gitlab, + exit_code, + no_exit_code, + }) => { // Determine the path to validate let validate_path = path .clone() @@ -271,6 +284,7 @@ async fn main() { // Determine if we're validating a GitLab pipeline based on the --gitlab flag or file detection let force_gitlab = *gitlab; + let mut validation_failed = false; if validate_path.is_dir() { // Validate all workflow files in the directory @@ -292,21 +306,30 @@ async fn main() { let path = entry.path(); let is_gitlab = force_gitlab || is_gitlab_pipeline(&path); - if is_gitlab { - validate_gitlab_pipeline(&path, verbose); + let file_failed = if is_gitlab { + validate_gitlab_pipeline(&path, verbose) } else { - validate_github_workflow(&path, verbose); + validate_github_workflow(&path, verbose) + }; + + if file_failed { + validation_failed = true; } } } else { // Validate a single workflow file let is_gitlab = force_gitlab || is_gitlab_pipeline(&validate_path); - if is_gitlab { - validate_gitlab_pipeline(&validate_path, verbose); + validation_failed = if is_gitlab { + validate_gitlab_pipeline(&validate_path, verbose) } else { - validate_github_workflow(&validate_path, verbose); - } + validate_github_workflow(&validate_path, verbose) + }; + } + + // Set exit code if validation failed and exit_code flag is true (and no_exit_code is false) + if validation_failed && *exit_code && !*no_exit_code { + std::process::exit(1); } } Some(Commands::Run { @@ -507,22 +530,32 @@ async fn main() { } /// Validate a GitHub workflow file -fn validate_github_workflow(path: &Path, verbose: bool) { +/// Returns true if validation failed, false if it passed +fn validate_github_workflow(path: &Path, verbose: bool) -> bool { print!("Validating GitHub workflow file: {}... ", path.display()); // Use the ui crate's validate_workflow function match ui::validate_workflow(path, verbose) { Ok(_) => { // The detailed validation output is already printed by the function + // We need to check if there were validation issues + // Since ui::validate_workflow doesn't return the validation result directly, + // we need to call the evaluator directly to get the result + match evaluator::evaluate_workflow_file(path, verbose) { + Ok(result) => !result.is_valid, + Err(_) => true, // Parse errors count as validation failure + } } Err(e) => { eprintln!("Error validating workflow: {}", e); + true // Any error counts as validation failure } } } /// Validate a GitLab CI/CD pipeline file -fn validate_gitlab_pipeline(path: &Path, verbose: bool) { +/// Returns true if validation failed, false if it passed +fn validate_gitlab_pipeline(path: &Path, verbose: bool) -> bool { print!("Validating GitLab CI pipeline file: {}... ", path.display()); // Parse and validate the pipeline file @@ -538,13 +571,18 @@ fn validate_gitlab_pipeline(path: &Path, verbose: bool) { for issue in validation_result.issues { println!(" - {}", issue); } - } else if verbose { - println!("✅ All validation checks passed"); + true // Validation failed + } else { + if verbose { + println!("✅ All validation checks passed"); + } + false // Validation passed } } Err(e) => { println!("❌ Invalid"); eprintln!("Validation failed: {}", e); + true // Parse error counts as validation failure } } }