mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2026-02-24 03:49:45 +01:00
feat: add --preserve-containers-on-failure flag for debugging
feat: add --preserve-containers-on-failure flag for debugging
This commit is contained in:
20
README.md
20
README.md
@@ -90,6 +90,9 @@ wrkflw run --emulate .github/workflows/ci.yml
|
||||
|
||||
# Run with verbose output
|
||||
wrkflw run --verbose .github/workflows/ci.yml
|
||||
|
||||
# Preserve failed containers for debugging
|
||||
wrkflw run --preserve-containers-on-failure .github/workflows/ci.yml
|
||||
```
|
||||
|
||||
### Using the TUI Interface
|
||||
@@ -223,6 +226,23 @@ WRKFLW supports composite actions, which are actions made up of multiple steps.
|
||||
|
||||
WRKFLW automatically cleans up any Docker containers created during workflow execution, even if the process is interrupted with Ctrl+C.
|
||||
|
||||
For debugging failed workflows, you can preserve containers that fail by using the `--preserve-containers-on-failure` flag:
|
||||
|
||||
```bash
|
||||
# Preserve failed containers for debugging
|
||||
wrkflw run --preserve-containers-on-failure .github/workflows/build.yml
|
||||
|
||||
# Also available in TUI mode
|
||||
wrkflw tui --preserve-containers-on-failure
|
||||
```
|
||||
|
||||
When a container fails with this flag enabled, WRKFLW will:
|
||||
- Keep the failed container running instead of removing it
|
||||
- Log the container ID and provide inspection instructions
|
||||
- Show a message like: `Preserving container abc123 for debugging (exit code: 1). Use 'docker exec -it abc123 bash' to inspect.`
|
||||
|
||||
This allows you to inspect the exact state of the container when the failure occurred, examine files, check environment variables, and debug issues more effectively.
|
||||
|
||||
## Limitations
|
||||
|
||||
### Supported Features
|
||||
|
||||
@@ -24,15 +24,23 @@ static CUSTOMIZED_IMAGES: Lazy<Mutex<HashMap<String, String>>> =
|
||||
|
||||
pub struct DockerRuntime {
|
||||
docker: Docker,
|
||||
preserve_containers_on_failure: bool,
|
||||
}
|
||||
|
||||
impl DockerRuntime {
|
||||
pub fn new() -> Result<Self, ContainerError> {
|
||||
Self::new_with_config(false)
|
||||
}
|
||||
|
||||
pub fn new_with_config(preserve_containers_on_failure: bool) -> Result<Self, ContainerError> {
|
||||
let docker = Docker::connect_with_local_defaults().map_err(|e| {
|
||||
ContainerError::ContainerStart(format!("Failed to connect to Docker: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(DockerRuntime { docker })
|
||||
Ok(DockerRuntime {
|
||||
docker,
|
||||
preserve_containers_on_failure,
|
||||
})
|
||||
}
|
||||
|
||||
// Add a method to store and retrieve customized images (e.g., with Python installed)
|
||||
@@ -998,13 +1006,23 @@ impl DockerRuntime {
|
||||
logging::warning("Retrieving container logs timed out");
|
||||
}
|
||||
|
||||
// Clean up container with a timeout
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
self.docker.remove_container(&container.id, None),
|
||||
)
|
||||
.await;
|
||||
untrack_container(&container.id);
|
||||
// Clean up container with a timeout, but preserve on failure if configured
|
||||
if exit_code == 0 || !self.preserve_containers_on_failure {
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
self.docker.remove_container(&container.id, None),
|
||||
)
|
||||
.await;
|
||||
untrack_container(&container.id);
|
||||
} else {
|
||||
// Container failed and we want to preserve it for debugging
|
||||
logging::info(&format!(
|
||||
"Preserving container {} for debugging (exit code: {}). Use 'docker exec -it {} bash' to inspect.",
|
||||
container.id, exit_code, container.id
|
||||
));
|
||||
// Still untrack it from the automatic cleanup system to prevent it from being cleaned up later
|
||||
untrack_container(&container.id);
|
||||
}
|
||||
|
||||
// Log detailed information about the command execution for debugging
|
||||
if exit_code != 0 {
|
||||
|
||||
@@ -24,19 +24,18 @@ use runtime::emulation;
|
||||
/// Execute a GitHub Actions workflow file locally
|
||||
pub async fn execute_workflow(
|
||||
workflow_path: &Path,
|
||||
runtime_type: RuntimeType,
|
||||
verbose: bool,
|
||||
config: ExecutionConfig,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
logging::info(&format!("Executing workflow: {}", workflow_path.display()));
|
||||
logging::info(&format!("Runtime: {:?}", runtime_type));
|
||||
logging::info(&format!("Runtime: {:?}", config.runtime_type));
|
||||
|
||||
// Determine if this is a GitLab CI/CD pipeline or GitHub Actions workflow
|
||||
let is_gitlab = is_gitlab_pipeline(workflow_path);
|
||||
|
||||
if is_gitlab {
|
||||
execute_gitlab_pipeline(workflow_path, runtime_type, verbose).await
|
||||
execute_gitlab_pipeline(workflow_path, config.clone()).await
|
||||
} else {
|
||||
execute_github_workflow(workflow_path, runtime_type, verbose).await
|
||||
execute_github_workflow(workflow_path, config.clone()).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +71,7 @@ fn is_gitlab_pipeline(path: &Path) -> bool {
|
||||
/// Execute a GitHub Actions workflow file locally
|
||||
async fn execute_github_workflow(
|
||||
workflow_path: &Path,
|
||||
runtime_type: RuntimeType,
|
||||
verbose: bool,
|
||||
config: ExecutionConfig,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
// 1. Parse workflow file
|
||||
let workflow = parse_workflow(workflow_path)?;
|
||||
@@ -82,7 +80,10 @@ async fn execute_github_workflow(
|
||||
let execution_plan = dependency::resolve_dependencies(&workflow)?;
|
||||
|
||||
// 3. Initialize appropriate runtime
|
||||
let runtime = initialize_runtime(runtime_type.clone())?;
|
||||
let runtime = initialize_runtime(
|
||||
config.runtime_type.clone(),
|
||||
config.preserve_containers_on_failure,
|
||||
)?;
|
||||
|
||||
// Create a temporary workspace directory
|
||||
let workspace_dir = tempfile::tempdir()
|
||||
@@ -94,7 +95,7 @@ async fn execute_github_workflow(
|
||||
// Add runtime mode to environment
|
||||
env_context.insert(
|
||||
"WRKFLW_RUNTIME_MODE".to_string(),
|
||||
if runtime_type == RuntimeType::Emulation {
|
||||
if config.runtime_type == RuntimeType::Emulation {
|
||||
"emulation".to_string()
|
||||
} else {
|
||||
"docker".to_string()
|
||||
@@ -124,7 +125,7 @@ async fn execute_github_workflow(
|
||||
&workflow,
|
||||
runtime.as_ref(),
|
||||
&env_context,
|
||||
verbose,
|
||||
config.verbose,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -164,8 +165,7 @@ async fn execute_github_workflow(
|
||||
/// Execute a GitLab CI/CD pipeline locally
|
||||
async fn execute_gitlab_pipeline(
|
||||
pipeline_path: &Path,
|
||||
runtime_type: RuntimeType,
|
||||
verbose: bool,
|
||||
config: ExecutionConfig,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
logging::info("Executing GitLab CI/CD pipeline");
|
||||
|
||||
@@ -180,7 +180,10 @@ async fn execute_gitlab_pipeline(
|
||||
let execution_plan = resolve_gitlab_dependencies(&pipeline, &workflow)?;
|
||||
|
||||
// 4. Initialize appropriate runtime
|
||||
let runtime = initialize_runtime(runtime_type.clone())?;
|
||||
let runtime = initialize_runtime(
|
||||
config.runtime_type.clone(),
|
||||
config.preserve_containers_on_failure,
|
||||
)?;
|
||||
|
||||
// Create a temporary workspace directory
|
||||
let workspace_dir = tempfile::tempdir()
|
||||
@@ -192,7 +195,7 @@ async fn execute_gitlab_pipeline(
|
||||
// Add runtime mode to environment
|
||||
env_context.insert(
|
||||
"WRKFLW_RUNTIME_MODE".to_string(),
|
||||
if runtime_type == RuntimeType::Emulation {
|
||||
if config.runtime_type == RuntimeType::Emulation {
|
||||
"emulation".to_string()
|
||||
} else {
|
||||
"docker".to_string()
|
||||
@@ -216,7 +219,7 @@ async fn execute_gitlab_pipeline(
|
||||
&workflow,
|
||||
runtime.as_ref(),
|
||||
&env_context,
|
||||
verbose,
|
||||
config.verbose,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -356,12 +359,13 @@ fn resolve_gitlab_dependencies(
|
||||
// Determine if Docker is available or fall back to emulation
|
||||
fn initialize_runtime(
|
||||
runtime_type: RuntimeType,
|
||||
preserve_containers_on_failure: bool,
|
||||
) -> Result<Box<dyn ContainerRuntime>, ExecutionError> {
|
||||
match runtime_type {
|
||||
RuntimeType::Docker => {
|
||||
if docker::is_available() {
|
||||
// Handle the Result returned by DockerRuntime::new()
|
||||
match docker::DockerRuntime::new() {
|
||||
match docker::DockerRuntime::new_with_config(preserve_containers_on_failure) {
|
||||
Ok(docker_runtime) => Ok(Box::new(docker_runtime)),
|
||||
Err(e) => {
|
||||
logging::error(&format!(
|
||||
@@ -386,6 +390,13 @@ pub enum RuntimeType {
|
||||
Emulation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutionConfig {
|
||||
pub runtime_type: RuntimeType,
|
||||
pub verbose: bool,
|
||||
pub preserve_containers_on_failure: bool,
|
||||
}
|
||||
|
||||
pub struct ExecutionResult {
|
||||
pub jobs: Vec<JobResult>,
|
||||
pub failure_details: Option<String>,
|
||||
|
||||
@@ -10,4 +10,6 @@ pub mod substitution;
|
||||
|
||||
// Re-export public items
|
||||
pub use docker::cleanup_resources;
|
||||
pub use engine::{execute_workflow, JobResult, JobStatus, RuntimeType, StepResult, StepStatus};
|
||||
pub use engine::{
|
||||
execute_workflow, ExecutionConfig, JobResult, JobStatus, RuntimeType, StepResult, StepStatus,
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ pub async fn run_wrkflw_tui(
|
||||
path: Option<&PathBuf>,
|
||||
runtime_type: RuntimeType,
|
||||
verbose: bool,
|
||||
preserve_containers_on_failure: bool,
|
||||
) -> io::Result<()> {
|
||||
// Terminal setup
|
||||
enable_raw_mode()?;
|
||||
@@ -41,7 +42,11 @@ pub async fn run_wrkflw_tui(
|
||||
) = mpsc::channel();
|
||||
|
||||
// Initialize app state
|
||||
let mut app = App::new(runtime_type.clone(), tx.clone());
|
||||
let mut app = App::new(
|
||||
runtime_type.clone(),
|
||||
tx.clone(),
|
||||
preserve_containers_on_failure,
|
||||
);
|
||||
|
||||
if app.validation_mode {
|
||||
app.logs.push("Starting in validation mode".to_string());
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct App {
|
||||
pub show_help: bool,
|
||||
pub runtime_type: RuntimeType,
|
||||
pub validation_mode: bool,
|
||||
pub preserve_containers_on_failure: bool,
|
||||
pub execution_queue: Vec<usize>, // Indices of workflows to execute
|
||||
pub current_execution: Option<usize>,
|
||||
pub logs: Vec<String>, // Overall execution logs
|
||||
@@ -42,7 +43,11 @@ pub struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(runtime_type: RuntimeType, tx: mpsc::Sender<ExecutionResultMsg>) -> App {
|
||||
pub fn new(
|
||||
runtime_type: RuntimeType,
|
||||
tx: mpsc::Sender<ExecutionResultMsg>,
|
||||
preserve_containers_on_failure: bool,
|
||||
) -> App {
|
||||
let mut workflow_list_state = ListState::default();
|
||||
workflow_list_state.select(Some(0));
|
||||
|
||||
@@ -119,6 +124,7 @@ impl App {
|
||||
show_help: false,
|
||||
runtime_type,
|
||||
validation_mode: false,
|
||||
preserve_containers_on_failure,
|
||||
execution_queue: Vec::new(),
|
||||
current_execution: None,
|
||||
logs: initial_logs,
|
||||
|
||||
@@ -127,7 +127,13 @@ pub async fn execute_workflow_cli(
|
||||
verbose
|
||||
));
|
||||
|
||||
match executor::execute_workflow(path, runtime_type, verbose).await {
|
||||
let config = executor::ExecutionConfig {
|
||||
runtime_type,
|
||||
verbose,
|
||||
preserve_containers_on_failure: false, // Default for this path
|
||||
};
|
||||
|
||||
match executor::execute_workflow(path, config).await {
|
||||
Ok(result) => {
|
||||
println!("\nWorkflow execution results:");
|
||||
|
||||
@@ -415,6 +421,7 @@ pub fn start_next_workflow_execution(
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -483,9 +490,15 @@ pub fn start_next_workflow_execution(
|
||||
}
|
||||
} else {
|
||||
// Use safe FD redirection for execution
|
||||
let config = executor::ExecutionConfig {
|
||||
runtime_type,
|
||||
verbose,
|
||||
preserve_containers_on_failure,
|
||||
};
|
||||
|
||||
let execution_result = utils::fd::with_stderr_to_null(|| {
|
||||
futures::executor::block_on(async {
|
||||
executor::execute_workflow(&workflow_path, runtime_type, verbose).await
|
||||
executor::execute_workflow(&workflow_path, config).await
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Failed to redirect stderr during execution: {}", e))?;
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::path::PathBuf;
|
||||
name = "wrkflw",
|
||||
about = "GitHub & GitLab CI/CD validator and executor",
|
||||
version,
|
||||
long_about = "A CI/CD 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 run .gitlab-ci.yml # Run a GitLab CI pipeline\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"
|
||||
long_about = "A CI/CD 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 run .gitlab-ci.yml # Run a GitLab CI pipeline\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\n wrkflw run --preserve-containers-on-failure .github/workflows/build.yml # Keep failed containers for debugging"
|
||||
)]
|
||||
struct Wrkflw {
|
||||
#[command(subcommand)]
|
||||
@@ -49,6 +49,10 @@ enum Commands {
|
||||
#[arg(long, default_value_t = false)]
|
||||
show_action_messages: bool,
|
||||
|
||||
/// Preserve Docker containers on failure for debugging (Docker mode only)
|
||||
#[arg(long)]
|
||||
preserve_containers_on_failure: bool,
|
||||
|
||||
/// Explicitly run as GitLab CI/CD pipeline
|
||||
#[arg(long)]
|
||||
gitlab: bool,
|
||||
@@ -66,6 +70,10 @@ enum Commands {
|
||||
/// Show 'Would execute GitHub action' messages in emulation mode
|
||||
#[arg(long, default_value_t = false)]
|
||||
show_action_messages: bool,
|
||||
|
||||
/// Preserve Docker containers on failure for debugging (Docker mode only)
|
||||
#[arg(long)]
|
||||
preserve_containers_on_failure: bool,
|
||||
},
|
||||
|
||||
/// Trigger a GitHub workflow remotely
|
||||
@@ -305,13 +313,18 @@ async fn main() {
|
||||
path,
|
||||
emulate,
|
||||
show_action_messages: _,
|
||||
preserve_containers_on_failure,
|
||||
gitlab,
|
||||
}) => {
|
||||
// Determine the runtime type
|
||||
let runtime_type = if *emulate {
|
||||
executor::RuntimeType::Emulation
|
||||
} else {
|
||||
executor::RuntimeType::Docker
|
||||
// Create execution configuration
|
||||
let config = executor::ExecutionConfig {
|
||||
runtime_type: if *emulate {
|
||||
executor::RuntimeType::Emulation
|
||||
} else {
|
||||
executor::RuntimeType::Docker
|
||||
},
|
||||
verbose,
|
||||
preserve_containers_on_failure: *preserve_containers_on_failure,
|
||||
};
|
||||
|
||||
// Check if we're explicitly or implicitly running a GitLab pipeline
|
||||
@@ -325,7 +338,7 @@ async fn main() {
|
||||
logging::info(&format!("Running {} at: {}", workflow_type, path.display()));
|
||||
|
||||
// Execute the workflow
|
||||
let result = executor::execute_workflow(path, runtime_type, verbose)
|
||||
let result = executor::execute_workflow(path, config)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Error executing workflow: {}", e);
|
||||
@@ -439,6 +452,7 @@ async fn main() {
|
||||
path,
|
||||
emulate,
|
||||
show_action_messages: _,
|
||||
preserve_containers_on_failure,
|
||||
}) => {
|
||||
// Set runtime type based on the emulate flag
|
||||
let runtime_type = if *emulate {
|
||||
@@ -448,7 +462,14 @@ async fn main() {
|
||||
};
|
||||
|
||||
// Call the TUI implementation from the ui crate
|
||||
if let Err(e) = ui::run_wrkflw_tui(path.as_ref(), runtime_type, verbose).await {
|
||||
if let Err(e) = ui::run_wrkflw_tui(
|
||||
path.as_ref(),
|
||||
runtime_type,
|
||||
verbose,
|
||||
*preserve_containers_on_failure,
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("Error running TUI: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -477,7 +498,7 @@ async fn main() {
|
||||
let runtime_type = executor::RuntimeType::Docker;
|
||||
|
||||
// Call the TUI implementation from the ui crate with default path
|
||||
if let Err(e) = ui::run_wrkflw_tui(None, runtime_type, verbose).await {
|
||||
if let Err(e) = ui::run_wrkflw_tui(None, runtime_type, verbose, false).await {
|
||||
eprintln!("Error running TUI: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user