Refactor: Migrate modules to workspace crates

- Extracted functionality from the `src/` directory into individual crates within the `crates/` directory. This improves modularity, organization, and separation of concerns.
- Migrated modules include: models, evaluator, ui, gitlab, utils, logging, github, matrix, executor, runtime, parser, and validators.
- Removed the original source files and directories from `src/` after successful migration.
- This change sets the stage for better code management and potentially independent development/versioning of workspace members.
This commit is contained in:
bahdotsh
2025-05-02 12:53:41 +05:30
parent 6ee550d39e
commit 470132c5bf
49 changed files with 1150 additions and 293 deletions

View File

@@ -1,90 +0,0 @@
# Test Organization for wrkflw
Following Rust best practices, we have reorganized the tests in this project to improve maintainability and clarity.
## Test Structure
Tests are now organized as follows:
### 1. Unit Tests
Unit tests remain in the source files using the `#[cfg(test)]` attribute. These tests are designed to test individual functions and small units of code in isolation.
Example:
```rust
// In src/matrix.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_function() {
// Test code here
}
}
```
### 2. Integration Tests
Integration tests have been moved to the `tests/` directory. These tests import and test the public API of the crate, ensuring that different components work together correctly.
- `tests/matrix_test.rs` - Tests for matrix expansion functionality
- `tests/reusable_workflow_test.rs` - Tests for reusable workflow validation
### 3. End-to-End Tests
End-to-end tests are also located in the `tests/` directory. These tests simulate real-world usage scenarios and often involve external dependencies like Docker.
- `tests/cleanup_test.rs` - Tests for cleanup functionality with Docker containers, networks, etc.
## Running Tests
You can run all tests using:
```bash
cargo test
```
To run only unit tests:
```bash
cargo test --lib
```
To run only integration tests:
```bash
cargo test --test matrix_test --test reusable_workflow_test
```
To run only end-to-end tests:
```bash
cargo test --test cleanup_test
```
To run a specific test:
```bash
cargo test test_name
```
## CI Configuration
Our CI workflow has been updated to run all types of tests separately, allowing for better isolation and clearer failure reporting:
```yaml
- name: Run unit tests
run: cargo test --lib --verbose
- name: Run integration tests
run: cargo test --test matrix_test --test reusable_workflow_test --verbose
- name: Run e2e tests (if Docker available)
run: cargo test --test cleanup_test --verbose -- --skip docker --skip processes
```
## Writing New Tests
When adding new tests:
1. For unit tests, add them to the relevant source file using `#[cfg(test)]`
2. For integration tests, add them to the `tests/` directory with a descriptive name like `feature_name_test.rs`
3. For end-to-end tests, also add them to the `tests/` directory with a descriptive name
Follow the existing patterns to ensure consistency.

View File

@@ -78,10 +78,6 @@ jobs:
target: aarch64-apple-darwin
artifact_name: wrkflw
asset_name: wrkflw-${{ github.event.inputs.version || github.ref_name }}-macos-arm64
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact_name: wrkflw.exe
asset_name: wrkflw-${{ github.event.inputs.version || github.ref_name }}-windows-x86_64
steps:
- name: Checkout code

View File

@@ -1,43 +0,0 @@
name: Rust
on:
workflow_dispatch:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
test-unit:
needs: [build]
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Run unit tests
run: cargo test --lib --verbose
test-integration:
needs: [build]
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Run integration tests
run: cargo test --test matrix_test --test reusable_workflow_test --verbose
test-e2e:
needs: [build]
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Run e2e tests (if Docker available)
run: cargo test --test cleanup_test --verbose -- --skip docker --skip processes

231
Cargo.lock generated
View File

@@ -486,6 +486,46 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "evaluator"
version = "0.4.0"
dependencies = [
"colored",
"models",
"serde_yaml",
"validators",
]
[[package]]
name = "executor"
version = "0.4.0"
dependencies = [
"async-trait",
"bollard",
"chrono",
"dirs",
"futures",
"futures-util",
"lazy_static",
"logging",
"matrix",
"models",
"num_cpus",
"once_cell",
"parser",
"regex",
"runtime",
"serde",
"serde_json",
"serde_yaml",
"tar",
"tempfile",
"thiserror",
"tokio",
"utils",
"uuid",
]
[[package]]
name = "fancy-regex"
version = "0.11.0"
@@ -674,6 +714,35 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "github"
version = "0.4.0"
dependencies = [
"lazy_static",
"models",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
"thiserror",
]
[[package]]
name = "gitlab"
version = "0.4.0"
dependencies = [
"lazy_static",
"models",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
"thiserror",
"urlencoding",
]
[[package]]
name = "h2"
version = "0.3.26"
@@ -729,15 +798,6 @@ 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"
@@ -1112,12 +1172,6 @@ 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"
@@ -1146,6 +1200,28 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "logging"
version = "0.4.0"
dependencies = [
"chrono",
"models",
"once_cell",
"serde",
"serde_yaml",
]
[[package]]
name = "matrix"
version = "0.4.0"
dependencies = [
"indexmap 2.8.0",
"models",
"serde",
"serde_yaml",
"thiserror",
]
[[package]]
name = "memchr"
version = "2.7.4"
@@ -1190,6 +1266,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "models"
version = "0.4.0"
dependencies = [
"serde",
"serde_json",
"serde_yaml",
"thiserror",
]
[[package]]
name = "native-tls"
version = "0.2.14"
@@ -1410,6 +1496,18 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "parser"
version = "0.4.0"
dependencies = [
"jsonschema",
"matrix",
"models",
"serde",
"serde_json",
"serde_yaml",
]
[[package]]
name = "paste"
version = "1.0.15"
@@ -1616,25 +1714,26 @@ dependencies = [
"winreg",
]
[[package]]
name = "runtime"
version = "0.4.0"
dependencies = [
"async-trait",
"logging",
"models",
"once_cell",
"serde",
"serde_yaml",
"tempfile",
"tokio",
]
[[package]]
name = "rustc-demangle"
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"
@@ -1644,7 +1743,7 @@ dependencies = [
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys 0.9.3",
"linux-raw-sys",
"windows-sys 0.59.0",
]
@@ -1954,7 +2053,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.3",
"rustix",
"windows-sys 0.59.0",
]
@@ -2102,6 +2201,28 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ui"
version = "0.4.0"
dependencies = [
"chrono",
"crossterm 0.26.1",
"evaluator",
"executor",
"futures",
"github",
"logging",
"models",
"ratatui",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
"tokio",
"utils",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
@@ -2161,6 +2282,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "utils"
version = "0.4.0"
dependencies = [
"models",
"nix",
"serde",
"serde_yaml",
]
[[package]]
name = "uuid"
version = "1.16.0"
@@ -2170,6 +2301,16 @@ dependencies = [
"getrandom 0.3.2",
]
[[package]]
name = "validators"
version = "0.4.0"
dependencies = [
"matrix",
"models",
"serde",
"serde_yaml",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -2287,18 +2428,6 @@ 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"
@@ -2519,38 +2648,46 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
name = "wrkflw"
version = "0.4.0"
dependencies = [
"async-trait",
"bollard",
"chrono",
"clap",
"colored",
"crossterm 0.26.1",
"dirs",
"evaluator",
"executor",
"futures",
"futures-util",
"github",
"gitlab",
"indexmap 2.8.0",
"itertools",
"jsonschema",
"lazy_static",
"libc",
"log",
"logging",
"matrix",
"models",
"nix",
"num_cpus",
"once_cell",
"parser",
"ratatui",
"rayon",
"regex",
"reqwest",
"runtime",
"serde",
"serde_json",
"serde_yaml",
"tar",
"tempfile",
"thiserror",
"tokio",
"ui",
"urlencoding",
"utils",
"uuid",
"which",
"validators",
]
[[package]]
@@ -2560,7 +2697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
dependencies = [
"libc",
"rustix 1.0.3",
"rustix",
]
[[package]]

View File

@@ -1,5 +1,10 @@
[package]
name = "wrkflw"
[workspace]
members = [
"crates/*"
]
resolver = "2"
[workspace.package]
version = "0.4.0"
edition = "2021"
description = "A GitHub Actions workflow validator and executor"
@@ -10,7 +15,7 @@ keywords = ["workflows", "github", "local"]
categories = ["command-line-utilities"]
license = "MIT"
[dependencies]
[workspace.dependencies]
clap = { version = "4.3", features = ["derive"] }
colored = "2.0"
serde = { version = "1.0", features = ["derive"] }

97
crates/README.md Normal file
View File

@@ -0,0 +1,97 @@
# Wrkflw Crates
This directory contains the Rust crates that make up the Wrkflw project. The project has been restructured to use a workspace-based approach with individual crates for better modularity and maintainability.
## Crate Structure
- **wrkflw**: Main binary crate and entry point for the application
- **models**: Data models and structures used throughout the application
- **evaluator**: Workflow evaluation functionality
- **executor**: Workflow execution engine
- **github**: GitHub API integration
- **gitlab**: GitLab API integration
- **logging**: Logging functionality
- **matrix**: Matrix-based parallelization support
- **parser**: Workflow parsing functionality
- **runtime**: Runtime execution environment
- **ui**: User interface components
- **utils**: Utility functions
- **validators**: Validation functionality
## Dependencies
Each crate has its own `Cargo.toml` file that defines its dependencies. The root `Cargo.toml` file defines the workspace and shared dependencies.
## Build Instructions
To build the entire project:
```bash
cargo build
```
To build a specific crate:
```bash
cargo build -p <crate-name>
```
## Testing
To run tests for the entire project:
```bash
cargo test
```
To run tests for a specific crate:
```bash
cargo test -p <crate-name>
```
## Rust Best Practices
When contributing to wrkflw, please follow these Rust best practices:
### Code Organization
- Place modules in their respective crates to maintain separation of concerns
- Use `pub` selectively to expose only the necessary APIs
- Follow the Rust module system conventions (use `mod` and `pub mod` appropriately)
### Errors and Error Handling
- Prefer using the `thiserror` crate for defining custom error types
- Use the `?` operator for error propagation instead of match statements when appropriate
- Implement custom error types that provide context for the error
- Avoid using `.unwrap()` and `.expect()` in production code
### Performance
- Profile code before optimizing using tools like `cargo flamegraph`
- Use `Arc` and `Mutex` judiciously for shared mutable state
- Leverage Rust's zero-cost abstractions (iterators, closures)
- Consider adding benchmark tests using the `criterion` crate for performance-critical code
### Security
- Validate all input, especially from external sources
- Avoid using `unsafe` code unless absolutely necessary
- Handle secrets securely using environment variables
- Check for integer overflows with `checked_` operations
### Testing
- Write unit tests for all public functions
- Use integration tests to verify crate-to-crate interactions
- Consider property-based testing for complex logic
- Structure tests with clear preparation, execution, and verification phases
### Tooling
- Run `cargo clippy` before committing changes to catch common mistakes
- Use `cargo fmt` to maintain consistent code formatting
- Enable compiler warnings with `#![warn(clippy::all)]`
For more detailed guidance, refer to the project's best practices documentation.

View File

@@ -0,0 +1,15 @@
[package]
name = "evaluator"
version.workspace = true
edition.workspace = true
description = "Workflow evaluation for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
validators = { path = "../validators" }
# External dependencies
colored.workspace = true
serde_yaml.workspace = true

View File

@@ -3,8 +3,8 @@ use serde_yaml::{self, Value};
use std::fs;
use std::path::Path;
use crate::models::ValidationResult;
use crate::validators::{validate_jobs, validate_triggers};
use models::ValidationResult;
use validators::{validate_jobs, validate_triggers};
pub fn evaluate_workflow_file(path: &Path, verbose: bool) -> Result<ValidationResult, String> {
let content = fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;

View File

@@ -0,0 +1,35 @@
[package]
name = "executor"
version.workspace = true
edition.workspace = true
description = "Workflow executor for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
parser = { path = "../parser" }
runtime = { path = "../runtime" }
logging = { path = "../logging" }
matrix = { path = "../matrix" }
utils = { path = "../utils" }
# External dependencies
async-trait.workspace = true
bollard.workspace = true
chrono.workspace = true
dirs.workspace = true
futures.workspace = true
futures-util.workspace = true
lazy_static.workspace = true
num_cpus.workspace = true
once_cell.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
tar.workspace = true
tempfile.workspace = true
thiserror.workspace = true
tokio.workspace = true
uuid.workspace = true

View File

@@ -1,4 +1,4 @@
use crate::parser::workflow::WorkflowDefinition;
use parser::workflow::WorkflowDefinition;
use std::collections::{HashMap, HashSet};
pub fn resolve_dependencies(workflow: &WorkflowDefinition) -> Result<Vec<Vec<String>>, String> {

View File

@@ -1,5 +1,3 @@
use crate::logging;
use crate::runtime::container::{ContainerError, ContainerOutput, ContainerRuntime};
use async_trait::async_trait;
use bollard::{
container::{Config, CreateContainerOptions},
@@ -8,10 +6,14 @@ use bollard::{
Docker,
};
use futures_util::StreamExt;
use logging;
use once_cell::sync::Lazy;
use runtime::container::{ContainerError, ContainerOutput, ContainerRuntime};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Mutex;
use utils;
use utils::fd;
static RUNNING_CONTAINERS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
static CREATED_NETWORKS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
@@ -37,21 +39,35 @@ impl DockerRuntime {
#[allow(dead_code)]
pub fn get_customized_image(base_image: &str, customization: &str) -> Option<String> {
let key = format!("{}:{}", base_image, customization);
let images = CUSTOMIZED_IMAGES.lock().unwrap();
images.get(&key).cloned()
match CUSTOMIZED_IMAGES.lock() {
Ok(images) => images.get(&key).cloned(),
Err(e) => {
logging::error(&format!("Failed to acquire lock: {}", e));
None
}
}
}
#[allow(dead_code)]
pub fn set_customized_image(base_image: &str, customization: &str, new_image: &str) {
let key = format!("{}:{}", base_image, customization);
let mut images = CUSTOMIZED_IMAGES.lock().unwrap();
images.insert(key, new_image.to_string());
if let Err(e) = CUSTOMIZED_IMAGES.lock().map(|mut images| {
images.insert(key, new_image.to_string());
}) {
logging::error(&format!("Failed to acquire lock: {}", e));
}
}
/// Find a customized image key by prefix
#[allow(dead_code)]
pub fn find_customized_image_key(image: &str, prefix: &str) -> Option<String> {
let image_keys = CUSTOMIZED_IMAGES.lock().unwrap();
let image_keys = match CUSTOMIZED_IMAGES.lock() {
Ok(keys) => keys,
Err(e) => {
logging::error(&format!("Failed to acquire lock: {}", e));
return None;
}
};
// Look for any key that starts with the prefix
for (key, _) in image_keys.iter() {
@@ -80,8 +96,13 @@ impl DockerRuntime {
(lang, None) => lang.to_string(),
};
let images = CUSTOMIZED_IMAGES.lock().unwrap();
images.get(&key).cloned()
match CUSTOMIZED_IMAGES.lock() {
Ok(images) => images.get(&key).cloned(),
Err(e) => {
logging::error(&format!("Failed to acquire lock: {}", e));
None
}
}
}
/// Set a customized image with language-specific dependencies
@@ -102,8 +123,11 @@ impl DockerRuntime {
(lang, None) => lang.to_string(),
};
let mut images = CUSTOMIZED_IMAGES.lock().unwrap();
images.insert(key, new_image.to_string());
if let Err(e) = CUSTOMIZED_IMAGES.lock().map(|mut images| {
images.insert(key, new_image.to_string());
}) {
logging::error(&format!("Failed to acquire lock: {}", e));
}
}
/// Prepare a language-specific environment
@@ -250,7 +274,7 @@ pub fn is_available() -> bool {
// Spawn a thread with the timeout to prevent blocking the main thread
let handle = std::thread::spawn(move || {
// Use safe FD redirection utility to suppress Docker error messages
match crate::utils::fd::with_stderr_to_null(|| {
match fd::with_stderr_to_null(|| {
// First, check if docker CLI is available as a quick test
if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
// Try a simple docker version command with a short timeout

View File

@@ -8,14 +8,14 @@ use std::path::Path;
use std::process::Command;
use thiserror::Error;
use crate::executor::dependency;
use crate::executor::docker;
use crate::executor::environment;
use crate::logging;
use crate::matrix::{self, MatrixCombination};
use crate::parser::workflow::{parse_workflow, ActionInfo, Job, WorkflowDefinition};
use crate::runtime::container::ContainerRuntime;
use crate::runtime::emulation::handle_special_action;
use crate::dependency;
use crate::docker;
use crate::environment;
use logging;
use matrix::MatrixCombination;
use parser::workflow::{self, parse_workflow, ActionInfo, Job, WorkflowDefinition};
use runtime::container::ContainerRuntime;
use runtime::emulation;
#[allow(unused_variables, unused_assignments)]
/// Execute a GitHub Actions workflow file locally
@@ -128,15 +128,15 @@ fn initialize_runtime(
"Failed to initialize Docker runtime: {}, falling back to emulation mode",
e
));
Ok(Box::new(crate::runtime::emulation::EmulationRuntime::new()))
Ok(Box::new(emulation::EmulationRuntime::new()))
}
}
} else {
logging::error("Docker not available, falling back to emulation mode");
Ok(Box::new(crate::runtime::emulation::EmulationRuntime::new()))
Ok(Box::new(emulation::EmulationRuntime::new()))
}
}
RuntimeType::Emulation => Ok(Box::new(crate::runtime::emulation::EmulationRuntime::new())),
RuntimeType::Emulation => Ok(Box::new(emulation::EmulationRuntime::new())),
}
}
@@ -907,7 +907,7 @@ async fn execute_matrix_job(
// Before the execute_step function, add this struct
struct StepExecutionContext<'a> {
step: &'a crate::parser::workflow::Step,
step: &'a workflow::Step,
step_idx: usize,
job_env: &'a HashMap<String, String>,
working_dir: &'a Path,
@@ -1183,7 +1183,7 @@ async fn execute_step(ctx: StepExecutionContext<'_>) -> Result<StepResult, Execu
}
} else {
// For GitHub actions, check if we have special handling
if let Err(e) = handle_special_action(uses).await {
if let Err(e) = emulation::handle_special_action(uses).await {
// Log error but continue
println!(" Warning: Special action handling failed: {}", e);
}
@@ -1538,9 +1538,11 @@ async fn execute_step(ctx: StepExecutionContext<'_>) -> Result<StepResult, Execu
output,
}
} else {
return Err(ExecutionError::Execution(
"Step must have either 'uses' or 'run' field".to_string(),
));
return Ok(StepResult {
name: step_name,
status: StepStatus::Skipped,
output: "Step has neither 'uses' nor 'run'".to_string(),
});
};
Ok(step_result)
@@ -1730,7 +1732,7 @@ fn extract_language_info(image: &str) -> Option<(&'static str, Option<&str>)> {
}
async fn execute_composite_action(
step: &crate::parser::workflow::Step,
step: &workflow::Step,
action_path: &Path,
job_env: &HashMap<String, String>,
working_dir: &Path,
@@ -1825,7 +1827,7 @@ async fn execute_composite_action(
job_env: &action_env,
working_dir,
runtime,
workflow: &crate::parser::workflow::WorkflowDefinition {
workflow: &workflow::WorkflowDefinition {
name: "Composite Action".to_string(),
on: vec![],
on_raw: serde_yaml::Value::Null,
@@ -1907,9 +1909,7 @@ async fn execute_composite_action(
}
// Helper function to convert YAML step to our Step struct
fn convert_yaml_to_step(
step_yaml: &serde_yaml::Value,
) -> Result<crate::parser::workflow::Step, String> {
fn convert_yaml_to_step(step_yaml: &serde_yaml::Value) -> Result<workflow::Step, String> {
// Extract step properties
let name = step_yaml
.get("name")
@@ -1961,7 +1961,7 @@ fn convert_yaml_to_step(
// Extract continue_on_error
let continue_on_error = step_yaml.get("continue-on-error").and_then(|v| v.as_bool());
Ok(crate::parser::workflow::Step {
Ok(workflow::Step {
name,
uses,
run: final_run,

View File

@@ -1,6 +1,6 @@
use crate::matrix::MatrixCombination;
use crate::parser::workflow::WorkflowDefinition;
use chrono::Utc;
use matrix::MatrixCombination;
use parser::workflow::WorkflowDefinition;
use serde_yaml::Value;
use std::{collections::HashMap, fs, io, path::Path};

View File

@@ -1,3 +1,5 @@
// executor crate
#![allow(unused_variables, unused_assignments)]
pub mod dependency;

19
crates/github/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "github"
version.workspace = true
edition.workspace = true
description = "github functionality for wrkflw"
license.workspace = true
[dependencies]
# Add other crate dependencies as needed
models = { path = "../models" }
# External dependencies from workspace
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
reqwest.workspace = true
thiserror.workspace = true
lazy_static.workspace = true
regex.workspace = true

View File

@@ -1,6 +1,9 @@
// github crate
use lazy_static::lazy_static;
use regex::Regex;
use reqwest::header;
use serde_json::{self};
use std::collections::HashMap;
use std::fs;
use std::path::Path;

20
crates/gitlab/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "gitlab"
version.workspace = true
edition.workspace = true
description = "gitlab functionality for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
# External dependencies
lazy_static.workspace = true
regex.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
thiserror.workspace = true
urlencoding.workspace = true

View File

@@ -1,3 +1,5 @@
// gitlab crate
use lazy_static::lazy_static;
use regex::Regex;
use reqwest::header;

16
crates/logging/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "logging"
version.workspace = true
edition.workspace = true
description = "logging functionality for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
# External dependencies
chrono.workspace = true
once_cell.workspace = true
serde.workspace = true
serde_yaml.workspace = true

View File

@@ -1,3 +1,5 @@
use chrono::Local;
use once_cell::sync::Lazy;
use std::sync::{Arc, Mutex};

16
crates/matrix/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "matrix"
version.workspace = true
edition.workspace = true
description = "matrix functionality for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
# External dependencies
indexmap.workspace = true
serde.workspace = true
serde_yaml.workspace = true
thiserror.workspace = true

View File

@@ -1,3 +1,5 @@
// matrix crate
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_yaml::Value;

12
crates/models/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "models"
version.workspace = true
edition.workspace = true
description = "Data models for wrkflw"
license.workspace = true
[dependencies]
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
thiserror.workspace = true

17
crates/parser/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "parser"
version.workspace = true
edition.workspace = true
description = "Parser functionality for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
matrix = { path = "../matrix" }
# External dependencies
jsonschema.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true

View File

@@ -1,2 +1,4 @@
// parser crate
pub mod schema;
pub mod workflow;

View File

@@ -3,7 +3,7 @@ use serde_json::Value;
use std::fs;
use std::path::Path;
const GITHUB_WORKFLOW_SCHEMA: &str = include_str!("../../schemas/github-workflow.json");
const GITHUB_WORKFLOW_SCHEMA: &str = include_str!("../../../schemas/github-workflow.json");
pub struct SchemaValidator {
schema: JSONSchema,

View File

@@ -1,4 +1,4 @@
use crate::matrix::MatrixConfig;
use matrix::MatrixConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;

19
crates/runtime/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "runtime"
version.workspace = true
edition.workspace = true
description = "Runtime environment for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
logging = { path = "../logging" }
# External dependencies
async-trait.workspace = true
once_cell.workspace = true
serde.workspace = true
serde_yaml.workspace = true
tempfile.workspace = true
tokio.workspace = true

View File

@@ -1,6 +1,6 @@
use crate::logging;
use crate::runtime::container::{ContainerError, ContainerOutput, ContainerRuntime};
use crate::container::{ContainerError, ContainerOutput, ContainerRuntime};
use async_trait::async_trait;
use logging;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::fs;

View File

@@ -1,2 +1,4 @@
// runtime crate
pub mod container;
pub mod emulation;

27
crates/ui/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "ui"
version.workspace = true
edition.workspace = true
description = "user interface functionality for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
evaluator = { path = "../evaluator" }
executor = { path = "../executor" }
logging = { path = "../logging" }
utils = { path = "../utils" }
github = { path = "../github" }
# External dependencies
chrono.workspace = true
crossterm.workspace = true
ratatui.workspace = true
serde.workspace = true
serde_yaml.workspace = true
tokio.workspace = true
serde_json.workspace = true
reqwest = { workspace = true, features = ["json"] }
regex.workspace = true
futures.workspace = true

View File

@@ -1,14 +1,13 @@
use crate::evaluator::evaluate_workflow_file;
use crate::executor::{self, JobStatus, RuntimeType, StepStatus};
use crate::logging;
use crate::utils;
use crate::utils::is_workflow_file;
// ui crate
use chrono::Local;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use evaluator::evaluate_workflow_file;
use executor::{self, JobStatus, RuntimeType, StepStatus};
use ratatui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
@@ -25,6 +24,7 @@ use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use utils::is_workflow_file;
// Represents an individual workflow file
struct Workflow {
@@ -743,7 +743,7 @@ impl App {
for log in &self.logs {
all_logs.push(log.clone());
}
for log in crate::logging::get_logs() {
for log in logging::get_logs() {
all_logs.push(log.clone());
}
@@ -835,7 +835,7 @@ impl App {
// Scroll logs down
fn scroll_logs_down(&mut self) {
// Get total log count including system logs
let total_logs = self.logs.len() + crate::logging::get_logs().len();
let total_logs = self.logs.len() + logging::get_logs().len();
if total_logs > 0 {
self.log_scroll = (self.log_scroll + 1).min(total_logs - 1);
}
@@ -2135,7 +2135,7 @@ fn render_logs_tab(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App, area:
}
// Process system logs
for log in crate::logging::get_logs() {
for log in logging::get_logs() {
all_logs.push(log.clone());
}
@@ -2335,7 +2335,7 @@ fn render_logs_tab(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App, area:
} else {
// This is a system log
let system_idx = original_idx - app.logs.len();
crate::logging::get_logs()
logging::get_logs()
.get(system_idx)
.map(|l| all_logs.iter().position(|al| al == l))
};
@@ -2741,7 +2741,7 @@ fn render_status_bar(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App, are
}
2 => {
// For logs tab, show scrolling instructions
let log_count = app.logs.len() + crate::logging::get_logs().len();
let log_count = app.logs.len() + logging::get_logs().len();
if log_count > 0 {
// Convert to a static string for consistent return type
let scroll_text = format!(
@@ -3300,8 +3300,8 @@ async fn execute_curl_trigger(
}
// Get repository information
let repo_info = crate::github::get_repo_info()
.map_err(|e| format!("Failed to get repository info: {}", e))?;
let repo_info =
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);

15
crates/utils/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "utils"
version.workspace = true
edition.workspace = true
description = "utility functions for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
# External dependencies
serde.workspace = true
serde_yaml.workspace = true
nix.workspace = true

View File

@@ -1,3 +1,5 @@
// utils crate
use std::path::Path;
pub fn is_workflow_file(path: &Path) -> bool {

View File

@@ -0,0 +1,15 @@
[package]
name = "validators"
version.workspace = true
edition.workspace = true
description = "validation functionality for wrkflw"
license.workspace = true
[dependencies]
# Internal crates
models = { path = "../models" }
matrix = { path = "../matrix" }
# External dependencies
serde.workspace = true
serde_yaml.workspace = true

View File

@@ -1,4 +1,4 @@
use crate::models::ValidationResult;
use models::ValidationResult;
pub fn validate_action_reference(
action_ref: &str,

View File

@@ -1,5 +1,5 @@
use crate::models::ValidationResult;
use crate::validators::{validate_matrix, validate_steps};
use crate::{validate_matrix, validate_steps};
use models::ValidationResult;
use serde_yaml::Value;
pub fn validate_jobs(jobs: &Value, result: &mut ValidationResult) {

View File

@@ -1,3 +1,5 @@
// validators crate
mod actions;
mod jobs;
mod matrix;

View File

@@ -1,4 +1,4 @@
use crate::models::ValidationResult;
use models::ValidationResult;
use serde_yaml::Value;
pub fn validate_matrix(matrix: &Value, result: &mut ValidationResult) {

View File

@@ -1,5 +1,5 @@
use crate::models::ValidationResult;
use crate::validators::validate_action_reference;
use crate::validate_action_reference;
use models::ValidationResult;
use serde_yaml::Value;
pub fn validate_steps(steps: &[Value], job_name: &str, result: &mut ValidationResult) {

View File

@@ -1,4 +1,4 @@
use crate::models::ValidationResult;
use models::ValidationResult;
use serde_yaml::Value;
pub fn validate_triggers(on: &Value, result: &mut ValidationResult) {

60
crates/wrkflw/Cargo.toml Normal file
View File

@@ -0,0 +1,60 @@
[package]
name = "wrkflw"
version.workspace = true
edition.workspace = true
description.workspace = true
documentation.workspace = true
homepage.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true
[dependencies]
# Workspace crates
models = { path = "../models" }
executor = { path = "../executor" }
github = { path = "../github" }
gitlab = { path = "../gitlab" }
logging = { path = "../logging" }
matrix = { path = "../matrix" }
parser = { path = "../parser" }
runtime = { path = "../runtime" }
ui = { path = "../ui" }
utils = { path = "../utils" }
validators = { path = "../validators" }
evaluator = { path = "../evaluator" }
# External dependencies
clap.workspace = true
bollard.workspace = true
tokio.workspace = true
futures-util.workspace = true
futures.workspace = true
chrono.workspace = true
uuid.workspace = true
tempfile.workspace = true
dirs.workspace = true
thiserror.workspace = true
log.workspace = true
regex.workspace = true
lazy_static.workspace = true
reqwest.workspace = true
libc.workspace = true
nix.workspace = true
urlencoding.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
colored.workspace = true
indexmap.workspace = true
rayon.workspace = true
num_cpus.workspace = true
itertools.workspace = true
once_cell.workspace = true
crossterm.workspace = true
ratatui.workspace = true
[[bin]]
name = "wrkflw"
path = "src/main.rs"

470
crates/wrkflw/src/main.rs Normal file
View File

@@ -0,0 +1,470 @@
use bollard::Docker;
use clap::{Parser, Subcommand};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(
name = "wrkflw",
about = "GitHub Workflow validator and executor",
version,
long_about = "A GitHub Workflow validator and executor that runs workflows locally.\n\nExamples:\n wrkflw validate # Validate all workflows in .github/workflows\n wrkflw run .github/workflows/build.yml # Run a specific workflow\n wrkflw --verbose run .github/workflows/build.yml # Run with more output\n wrkflw --debug run .github/workflows/build.yml # Run with detailed debug information\n wrkflw run --emulate .github/workflows/build.yml # Use emulation mode instead of Docker"
)]
struct Wrkflw {
#[command(subcommand)]
command: Option<Commands>,
/// Run in verbose mode with detailed output
#[arg(short, long, global = true)]
verbose: bool,
/// Run in debug mode with extensive execution details
#[arg(short, long, global = true)]
debug: bool,
}
#[derive(Debug, Subcommand)]
enum Commands {
/// Validate GitHub workflow files
Validate {
/// Path to workflow file or directory (defaults to .github/workflows)
path: Option<PathBuf>,
},
/// Execute GitHub workflow files locally
Run {
/// Path to workflow file to execute
path: PathBuf,
/// Use emulation mode instead of Docker
#[arg(short, long)]
emulate: bool,
/// Show 'Would execute GitHub action' messages in emulation mode
#[arg(long, default_value_t = false)]
show_action_messages: bool,
},
/// Open TUI interface to manage workflows
Tui {
/// Path to workflow file or directory (defaults to .github/workflows)
path: Option<PathBuf>,
/// Use emulation mode instead of Docker
#[arg(short, long)]
emulate: bool,
/// Show 'Would execute GitHub action' messages in emulation mode
#[arg(long, default_value_t = false)]
show_action_messages: bool,
},
/// Trigger a GitHub workflow remotely
Trigger {
/// Name of the workflow file (without .yml extension)
workflow: String,
/// Branch to run the workflow on
#[arg(short, long)]
branch: Option<String>,
/// Key-value inputs for the workflow in format key=value
#[arg(short, long, value_parser = parse_key_val)]
input: Option<Vec<(String, String)>>,
},
/// Trigger a GitLab pipeline remotely
TriggerGitlab {
/// Branch to run the pipeline on
#[arg(short, long)]
branch: Option<String>,
/// Key-value variables for the pipeline in format key=value
#[arg(short = 'V', long, value_parser = parse_key_val)]
variable: Option<Vec<(String, String)>>,
},
/// List available workflows
List,
}
// Parser function for key-value pairs
fn parse_key_val(s: &str) -> Result<(String, String), String> {
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?;
Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
}
/// Clean up all resources when exiting the application
/// This is used by both main.rs and in tests
pub async fn cleanup_on_exit() {
// Clean up Docker resources if available, but don't let it block indefinitely
match tokio::time::timeout(std::time::Duration::from_secs(3), async {
match Docker::connect_with_local_defaults() {
Ok(docker) => {
let _ = executor::docker::cleanup_containers(&docker).await;
}
Err(_) => {
// Docker not available
logging::info("Docker not available, skipping Docker cleanup");
}
}
})
.await
{
Ok(_) => logging::debug("Docker cleanup completed successfully"),
Err(_) => {
logging::warning("Docker cleanup timed out after 3 seconds, continuing with shutdown")
}
}
// Always clean up emulation resources
match tokio::time::timeout(
std::time::Duration::from_secs(2),
runtime::emulation::cleanup_resources(),
)
.await
{
Ok(_) => logging::debug("Emulation cleanup completed successfully"),
Err(_) => logging::warning("Emulation cleanup timed out, continuing with shutdown"),
}
logging::info("Resource cleanup completed");
}
async fn handle_signals() {
// Set up a hard exit timer in case cleanup takes too long
// This ensures the app always exits even if Docker operations are stuck
let hard_exit_time = std::time::Duration::from_secs(10);
// Wait for Ctrl+C
match tokio::signal::ctrl_c().await {
Ok(_) => {
println!("Received Ctrl+C, shutting down and cleaning up...");
}
Err(e) => {
// Log the error but continue with cleanup
eprintln!("Warning: Failed to properly listen for ctrl+c event: {}", e);
println!("Shutting down and cleaning up...");
}
}
// Set up a watchdog thread that will force exit if cleanup takes too long
// This is important because Docker operations can sometimes hang indefinitely
let _ = std::thread::spawn(move || {
std::thread::sleep(hard_exit_time);
eprintln!(
"Cleanup taking too long (over {} seconds), forcing exit...",
hard_exit_time.as_secs()
);
logging::error("Forced exit due to cleanup timeout");
std::process::exit(1);
});
// Clean up containers
cleanup_on_exit().await;
// Exit with success status - the force exit thread will be terminated automatically
std::process::exit(0);
}
#[tokio::main]
async fn main() {
let cli = Wrkflw::parse();
let verbose = cli.verbose;
let debug = cli.debug;
// Set log level based on command line flags
if debug {
logging::set_log_level(logging::LogLevel::Debug);
logging::debug("Debug mode enabled - showing detailed logs");
} else if verbose {
logging::set_log_level(logging::LogLevel::Info);
logging::info("Verbose mode enabled");
} else {
logging::set_log_level(logging::LogLevel::Warning);
}
// Setup a Ctrl+C handler that runs in the background
tokio::spawn(handle_signals());
match &cli.command {
Some(Commands::Validate { path }) => {
// Determine the path to validate
let validate_path = path
.clone()
.unwrap_or_else(|| PathBuf::from(".github/workflows"));
// Run the validation
ui::validate_workflow(&validate_path, verbose).unwrap_or_else(|e| {
eprintln!("Error: {}", e);
std::process::exit(1);
});
}
Some(Commands::Run {
path,
emulate,
show_action_messages: _,
}) => {
// Set runner mode based on flags
let runtime_type = if *emulate {
executor::RuntimeType::Emulation
} else {
executor::RuntimeType::Docker
};
// First validate the workflow file
match parser::workflow::parse_workflow(path) {
Ok(_) => logging::info("Validating workflow..."),
Err(e) => {
logging::error(&format!("Workflow validation failed: {}", e));
std::process::exit(1);
}
}
// Execute the workflow
match executor::execute_workflow(path, runtime_type, verbose || debug).await {
Ok(result) => {
// Print job results
for job in &result.jobs {
println!(
"\n{} Job {}: {}",
if job.status == executor::JobStatus::Success {
""
} else {
""
},
job.name,
if job.status == executor::JobStatus::Success {
"succeeded"
} else {
"failed"
}
);
// Print step results
for step in &job.steps {
println!(
" {} {}",
if step.status == executor::StepStatus::Success {
""
} else {
""
},
step.name
);
if !step.output.trim().is_empty() {
// If the output is very long, trim it
let output_lines = step.output.lines().collect::<Vec<&str>>();
println!(" Output:");
// In verbose mode, show complete output
if verbose || debug {
for line in &output_lines {
println!(" {}", line);
}
} else {
// Show only the first few lines
let max_lines = 5;
for line in output_lines.iter().take(max_lines) {
println!(" {}", line);
}
if output_lines.len() > max_lines {
println!(" ... ({} more lines, use --verbose to see full output)",
output_lines.len() - max_lines);
}
}
}
}
}
// Print detailed failure information if available
if let Some(failure_details) = &result.failure_details {
println!("\n❌ Workflow execution failed!");
println!("{}", failure_details);
println!("\nTo fix these issues:");
println!("1. Check the formatting issues with: cargo fmt");
println!("2. Fix clippy warnings with: cargo clippy -- -D warnings");
println!("3. Run tests to ensure everything passes: cargo test");
std::process::exit(1);
} else {
println!("\n✅ Workflow completed successfully!");
}
}
Err(e) => {
logging::error(&format!("Workflow execution failed: {}", e));
std::process::exit(1);
}
}
}
Some(Commands::Tui {
path,
emulate,
show_action_messages,
}) => {
// Open the TUI interface
let runtime_type = if *emulate {
executor::RuntimeType::Emulation
} else {
// Check if Docker is available, fall back to emulation if not
if !executor::docker::is_available() {
println!("⚠️ Docker is not available. Using emulation mode instead.");
logging::warning("Docker is not available. Using emulation mode instead.");
executor::RuntimeType::Emulation
} else {
executor::RuntimeType::Docker
}
};
// Control hiding action messages based on the flag
if !show_action_messages {
std::env::set_var("WRKFLW_HIDE_ACTION_MESSAGES", "true");
} else {
std::env::set_var("WRKFLW_HIDE_ACTION_MESSAGES", "false");
}
match ui::run_wrkflw_tui(path.as_ref(), runtime_type, verbose).await {
Ok(_) => {
// Clean up on successful exit
cleanup_on_exit().await;
}
Err(e) => {
eprintln!("Error: {}", e);
cleanup_on_exit().await;
std::process::exit(1);
}
}
}
Some(Commands::Trigger {
workflow,
branch,
input,
}) => {
logging::info(&format!("Triggering workflow {} on GitHub", workflow));
// Convert inputs to HashMap
let input_map = input.as_ref().map(|i| {
i.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<HashMap<String, String>>()
});
match github::trigger_workflow(workflow, branch.as_deref(), input_map).await {
Ok(_) => logging::info("Workflow triggered successfully"),
Err(e) => {
eprintln!("Error triggering workflow: {}", e);
std::process::exit(1);
}
}
}
Some(Commands::TriggerGitlab { branch, variable }) => {
logging::info("Triggering pipeline on GitLab");
// Convert variables to HashMap
let variable_map = variable.as_ref().map(|v| {
v.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<HashMap<String, String>>()
});
match gitlab::trigger_pipeline(branch.as_deref(), variable_map).await {
Ok(_) => logging::info("GitLab pipeline triggered successfully"),
Err(e) => {
eprintln!("Error triggering GitLab pipeline: {}", e);
std::process::exit(1);
}
}
}
Some(Commands::List) => {
logging::info("Listing available workflows");
// Attempt to get GitHub repo info
if let Ok(repo_info) = github::get_repo_info() {
match github::list_workflows(&repo_info).await {
Ok(workflows) => {
if workflows.is_empty() {
println!("No GitHub workflows found in repository");
} else {
println!("GitHub workflows:");
for workflow in workflows {
println!(" {}", workflow);
}
}
}
Err(e) => {
eprintln!("Error listing GitHub workflows: {}", e);
}
}
} else {
println!("Not a GitHub repository or unable to get repository information");
}
// Attempt to get GitLab repo info
if let Ok(repo_info) = gitlab::get_repo_info() {
match gitlab::list_pipelines(&repo_info).await {
Ok(pipelines) => {
if pipelines.is_empty() {
println!("No GitLab pipelines found in repository");
} else {
println!("GitLab pipelines:");
for pipeline in pipelines {
println!(" {}", pipeline);
}
}
}
Err(e) => {
eprintln!("Error listing GitLab pipelines: {}", e);
}
}
} else {
println!("Not a GitLab repository or unable to get repository information");
}
}
None => {
// Default to TUI interface if no subcommand
// Check if Docker is available, fall back to emulation if not
let runtime_type = if !executor::docker::is_available() {
println!("⚠️ Docker is not available. Using emulation mode instead.");
logging::warning("Docker is not available. Using emulation mode instead.");
executor::RuntimeType::Emulation
} else {
executor::RuntimeType::Docker
};
// Set environment variable to hide action messages by default
std::env::set_var("WRKFLW_HIDE_ACTION_MESSAGES", "true");
match ui::run_wrkflw_tui(
Some(&PathBuf::from(".github/workflows")),
runtime_type,
verbose,
)
.await
{
Ok(_) => {
// Clean up on successful exit
cleanup_on_exit().await;
}
Err(e) => {
eprintln!("Error: {}", e);
cleanup_on_exit().await;
std::process::exit(1);
}
}
}
}
// Final cleanup before program exit
cleanup_on_exit().await;
}

View File

@@ -1,44 +0,0 @@
#[async_trait]
impl ContainerRuntime for EmulationRuntime {
async fn run_container(
&self,
image: &str,
cmd: &[&str],
env_vars: &[(&str, &str)],
working_dir: &Path,
volumes: &[(&Path, &Path)],
) -> Result<ContainerOutput, ContainerError> {
// ... existing code ...
}
async fn pull_image(&self, image: &str) -> Result<(), ContainerError> {
// ... existing code ...
}
async fn build_image(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
// ... existing code ...
}
async fn prepare_language_environment(
&self,
language: &str,
version: Option<&str>,
additional_packages: Option<Vec<String>>,
) -> Result<String, ContainerError> {
// For emulation runtime, we'll use a simplified approach
// that doesn't require building custom images
let base_image = match language {
"python" => version.map_or("python:3.11-slim".to_string(), |v| format!("python:{}", v)),
"node" => version.map_or("node:20-slim".to_string(), |v| format!("node:{}", v)),
"java" => version.map_or("eclipse-temurin:17-jdk".to_string(), |v| format!("eclipse-temurin:{}", v)),
"go" => version.map_or("golang:1.21-slim".to_string(), |v| format!("golang:{}", v)),
"dotnet" => version.map_or("mcr.microsoft.com/dotnet/sdk:7.0".to_string(), |v| format!("mcr.microsoft.com/dotnet/sdk:{}", v)),
"rust" => version.map_or("rust:latest".to_string(), |v| format!("rust:{}", v)),
_ => return Err(ContainerError::ContainerStart(format!("Unsupported language: {}", language))),
};
// For emulation, we'll just return the base image
// The actual package installation will be handled during container execution
Ok(base_image)
}
}