make clippy happy

This commit is contained in:
bahdotsh
2025-04-21 18:04:52 +05:30
parent 7b735796c4
commit 90b3f6bac6
11 changed files with 638 additions and 630 deletions

View File

@@ -22,7 +22,7 @@ pub fn evaluate_workflow_file(path: &Path, verbose: bool) -> Result<ValidationRe
}
// Check if name exists
if !workflow.get("name").is_some() {
if workflow.get("name").is_none() {
result.add_issue("Workflow is missing a name".to_string());
}

View File

@@ -22,7 +22,7 @@ pub struct DockerRuntime {
impl DockerRuntime {
pub fn new() -> Result<Self, ContainerError> {
let docker = Docker::connect_with_local_defaults().map_err(|e| {
ContainerError::ContainerStartFailed(format!("Failed to connect to Docker: {}", e))
ContainerError::ContainerStart(format!("Failed to connect to Docker: {}", e))
})?;
Ok(DockerRuntime { docker })
@@ -141,11 +141,11 @@ pub async fn create_job_network(docker: &Docker) -> Result<String, ContainerErro
let network = docker
.create_network(options)
.await
.map_err(|e| ContainerError::NetworkCreationFailed(e.to_string()))?;
.map_err(|e| ContainerError::NetworkCreation(e.to_string()))?;
// network.id is Option<String>, unwrap it safely
let network_id = network.id.ok_or_else(|| {
ContainerError::NetworkOperationFailed("Network created but no ID returned".to_string())
ContainerError::NetworkOperation("Network created but no ID returned".to_string())
})?;
track_network(&network_id);
@@ -268,13 +268,13 @@ impl ContainerRuntime for DockerRuntime {
.docker
.create_container(options, config)
.await
.map_err(|e| ContainerError::ContainerStartFailed(e.to_string()))?;
.map_err(|e| ContainerError::ContainerStart(e.to_string()))?;
// Start container
self.docker
.start_container::<String>(&container.id, None)
.await
.map_err(|e| ContainerError::ContainerExecutionFailed(e.to_string()))?;
.map_err(|e| ContainerError::ContainerExecution(e.to_string()))?;
track_container(&container.id);
@@ -300,17 +300,15 @@ impl ContainerRuntime for DockerRuntime {
let mut stdout = String::new();
let mut stderr = String::new();
for log_result in logs {
if let Ok(log) = log_result {
match log {
bollard::container::LogOutput::StdOut { message } => {
stdout.push_str(&String::from_utf8_lossy(&message));
}
bollard::container::LogOutput::StdErr { message } => {
stderr.push_str(&String::from_utf8_lossy(&message));
}
_ => {}
for log in logs.into_iter().flatten() {
match log {
bollard::container::LogOutput::StdOut { message } => {
stdout.push_str(&String::from_utf8_lossy(&message));
}
bollard::container::LogOutput::StdErr { message } => {
stderr.push_str(&String::from_utf8_lossy(&message));
}
_ => {}
}
}
@@ -335,7 +333,7 @@ impl ContainerRuntime for DockerRuntime {
while let Some(result) = stream.next().await {
if let Err(e) = result {
return Err(ContainerError::ImagePullFailed(e.to_string()));
return Err(ContainerError::ImagePull(e.to_string()));
}
}
@@ -352,7 +350,7 @@ impl ContainerRuntime for DockerRuntime {
if let Ok(file) = std::fs::File::open(dockerfile) {
let mut header = tar::Header::new_gnu();
let metadata = file.metadata().map_err(|e| {
ContainerError::ContainerExecutionFailed(format!(
ContainerError::ContainerExecution(format!(
"Failed to get file metadata: {}",
e
))
@@ -360,14 +358,14 @@ impl ContainerRuntime for DockerRuntime {
let modified_time = metadata
.modified()
.map_err(|e| {
ContainerError::ContainerExecutionFailed(format!(
ContainerError::ContainerExecution(format!(
"Failed to get file modification time: {}",
e
))
})?
.elapsed()
.map_err(|e| {
ContainerError::ContainerExecutionFailed(format!(
ContainerError::ContainerExecution(format!(
"Failed to get elapsed time since modification: {}",
e
))
@@ -380,9 +378,9 @@ impl ContainerRuntime for DockerRuntime {
tar_builder
.append_data(&mut header, "Dockerfile", file)
.map_err(|e| ContainerError::ImageBuildFailed(e.to_string()))?;
.map_err(|e| ContainerError::ImageBuild(e.to_string()))?;
} else {
return Err(ContainerError::ImageBuildFailed(format!(
return Err(ContainerError::ImageBuild(format!(
"Cannot open Dockerfile at {}",
dockerfile.display()
)));
@@ -390,7 +388,7 @@ impl ContainerRuntime for DockerRuntime {
tar_builder
.into_inner()
.map_err(|e| ContainerError::ImageBuildFailed(e.to_string()))?
.map_err(|e| ContainerError::ImageBuild(e.to_string()))?
};
let options = bollard::image::BuildImageOptions {
@@ -412,7 +410,7 @@ impl ContainerRuntime for DockerRuntime {
// For verbose output, we could log the build progress here
}
Err(e) => {
return Err(ContainerError::ImageBuildFailed(e.to_string()));
return Err(ContainerError::ImageBuild(e.to_string()));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -113,7 +113,7 @@ fn value_to_string(value: &Value) -> String {
Value::Sequence(seq) => {
let items = seq
.iter()
.map(|v| value_to_string(v))
.map(value_to_string)
.collect::<Vec<_>>()
.join(",");
items
@@ -134,7 +134,7 @@ fn value_to_string(value: &Value) -> String {
fn get_repo_name() -> String {
// Try to detect from git if available
if let Ok(output) = std::process::Command::new("git")
.args(&["remote", "get-url", "origin"])
.args(["remote", "get-url", "origin"])
.output()
{
if output.status.success() {
@@ -200,7 +200,7 @@ fn get_workspace_path() -> String {
fn get_current_sha() -> String {
if let Ok(output) = std::process::Command::new("git")
.args(&["rev-parse", "HEAD"])
.args(["rev-parse", "HEAD"])
.output()
{
if output.status.success() {
@@ -213,7 +213,7 @@ fn get_current_sha() -> String {
fn get_current_ref() -> String {
if let Ok(output) = std::process::Command::new("git")
.args(&["symbolic-ref", "--short", "HEAD"])
.args(["symbolic-ref", "--short", "HEAD"])
.output()
{
if output.status.success() {

View File

@@ -1,8 +1,6 @@
use lazy_static::lazy_static;
use regex::Regex;
use reqwest;
use reqwest::header;
use serde_json;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
@@ -126,7 +124,7 @@ pub async fn list_workflows(_repo_info: &RepoInfo) -> Result<Vec<String>, Github
if path.is_file()
&& path
.extension()
.map_or(false, |ext| ext == "yml" || ext == "yaml")
.is_some_and(|ext| ext == "yml" || ext == "yaml")
{
if let Some(file_name) = path.file_stem() {
if let Some(name) = file_name.to_str() {
@@ -195,7 +193,7 @@ pub async fn trigger_workflow(
.json(&payload)
.send()
.await
.map_err(|e| GithubError::RequestError(e))?;
.map_err(GithubError::RequestError)?;
if !response.status().is_success() {
let status = response.status().as_u16();

View File

@@ -27,29 +27,29 @@ use std::fmt;
#[derive(Debug)]
pub enum ContainerError {
ImagePullFailed(String),
ImageBuildFailed(String),
ContainerStartFailed(String),
ContainerExecutionFailed(String),
NetworkCreationFailed(String),
NetworkOperationFailed(String),
ImagePull(String),
ImageBuild(String),
ContainerStart(String),
ContainerExecution(String),
NetworkCreation(String),
NetworkOperation(String),
}
impl fmt::Display for ContainerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContainerError::ImagePullFailed(msg) => write!(f, "Failed to pull image: {}", msg),
ContainerError::ImageBuildFailed(msg) => write!(f, "Failed to build image: {}", msg),
ContainerError::ContainerStartFailed(msg) => {
ContainerError::ImagePull(msg) => write!(f, "Failed to pull image: {}", msg),
ContainerError::ImageBuild(msg) => write!(f, "Failed to build image: {}", msg),
ContainerError::ContainerStart(msg) => {
write!(f, "Failed to start container: {}", msg)
}
ContainerError::ContainerExecutionFailed(msg) => {
ContainerError::ContainerExecution(msg) => {
write!(f, "Container execution failed: {}", msg)
}
ContainerError::NetworkCreationFailed(msg) => {
ContainerError::NetworkCreation(msg) => {
write!(f, "Failed to create Docker network: {}", msg)
}
ContainerError::NetworkOperationFailed(msg) => {
ContainerError::NetworkOperation(msg) => {
write!(f, "Network operation failed: {}", msg)
}
}

View File

@@ -75,32 +75,33 @@ impl EmulationRuntime {
fs::create_dir_all(&target_path).expect("Failed to create target directory");
// Copy files in this directory (not recursive for simplicity)
for entry in fs::read_dir(host_path).expect("Failed to read source directory") {
if let Ok(entry) = entry {
let source = entry.path();
let file_name = match source.file_name() {
Some(name) => name,
None => {
eprintln!(
"Warning: Could not get file name from path: {:?}",
source
);
continue; // Skip this file
}
};
let dest = target_path.join(file_name);
if source.is_file() {
if let Err(e) = fs::copy(&source, &dest) {
eprintln!(
"Warning: Failed to copy file from {:?} to {:?}: {}",
&source, &dest, e
);
}
} else {
// We could make this recursive if needed
fs::create_dir_all(&dest).expect("Failed to create subdirectory");
for entry in fs::read_dir(host_path)
.expect("Failed to read source directory")
.flatten()
{
let source = entry.path();
let file_name = match source.file_name() {
Some(name) => name,
None => {
eprintln!(
"Warning: Could not get file name from path: {:?}",
source
);
continue; // Skip this file
}
};
let dest = target_path.join(file_name);
if source.is_file() {
if let Err(e) = fs::copy(&source, &dest) {
eprintln!(
"Warning: Failed to copy file from {:?} to {:?}: {}",
&source, &dest, e
);
}
} else {
// We could make this recursive if needed
fs::create_dir_all(&dest).expect("Failed to create subdirectory");
}
}
}
@@ -157,7 +158,7 @@ impl ContainerRuntime for EmulationRuntime {
});
if is_long_running {
logging::info(&format!("Detected long-running command, will run detached"));
logging::info("Detected long-running command, will run detached");
let mut command = Command::new(cmd[0]);
command.current_dir(&container_working_dir);
@@ -186,7 +187,7 @@ impl ContainerRuntime for EmulationRuntime {
});
}
Err(e) => {
return Err(ContainerError::ContainerExecutionFailed(format!(
return Err(ContainerError::ContainerExecution(format!(
"Failed to start detached process: {}",
e
)));
@@ -205,12 +206,10 @@ impl ContainerRuntime for EmulationRuntime {
.unwrap_or(false);
if !nix_installed {
logging::info(&format!(
"⚠️ Nix commands detected but Nix is not installed!"
));
logging::info(&format!(
"🔄 To use this workflow, please install Nix: https://nixos.org/download.html"
));
logging::info("⚠️ Nix commands detected but Nix is not installed!");
logging::info(
"🔄 To use this workflow, please install Nix: https://nixos.org/download.html",
);
return Ok(ContainerOutput {
stdout: String::new(),
@@ -218,13 +217,13 @@ impl ContainerRuntime for EmulationRuntime {
exit_code: 1,
});
} else {
logging::info(&format!("✅ Nix is installed, proceeding with command"));
logging::info("✅ Nix is installed, proceeding with command");
}
}
// Ensure we have a command
if cmd.is_empty() {
return Err(ContainerError::ContainerExecutionFailed(
return Err(ContainerError::ContainerExecution(
"No command specified".to_string(),
));
}
@@ -266,8 +265,8 @@ impl ContainerRuntime for EmulationRuntime {
command.current_dir(&container_working_dir);
// Add flags
for i in 1..idx + 1 {
command.arg(cmd[i]);
for arg in cmd.iter().skip(1).take(idx) {
command.arg(arg);
}
// Add the command
@@ -281,7 +280,7 @@ impl ContainerRuntime for EmulationRuntime {
// Execute
let output = command
.output()
.map_err(|e| ContainerError::ContainerExecutionFailed(e.to_string()))?;
.map_err(|e| ContainerError::ContainerExecution(e.to_string()))?;
return Ok(ContainerOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
@@ -308,16 +307,14 @@ impl ContainerRuntime for EmulationRuntime {
}
// Log that we're running a background process
logging::info(&format!(
"Emulation: Running command with background processes"
));
logging::info("Emulation: Running command with background processes");
// For commands with background processes, we could potentially track PIDs
// However, since they're in a shell wrapper, we'd need to parse them from output
let output = shell_command
.output()
.map_err(|e| ContainerError::ContainerExecutionFailed(e.to_string()))?;
.map_err(|e| ContainerError::ContainerExecution(e.to_string()))?;
return Ok(ContainerOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
@@ -343,7 +340,7 @@ impl ContainerRuntime for EmulationRuntime {
// Execute
let output = command
.output()
.map_err(|e| ContainerError::ContainerExecutionFailed(e.to_string()))?;
.map_err(|e| ContainerError::ContainerExecution(e.to_string()))?;
Ok(ContainerOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
@@ -413,7 +410,7 @@ fn copy_directory_contents(source: &Path, dest: &Path) -> std::io::Result<()> {
pub async fn handle_special_action(action: &str) -> Result<(), ContainerError> {
if action.starts_with("cachix/install-nix-action") {
logging::info(&format!("🔄 Emulating cachix/install-nix-action"));
logging::info("🔄 Emulating cachix/install-nix-action");
// In emulation mode, check if nix is installed
let nix_installed = Command::new("which")
@@ -423,15 +420,13 @@ pub async fn handle_special_action(action: &str) -> Result<(), ContainerError> {
.unwrap_or(false);
if !nix_installed {
logging::info(&format!("🔄 Emulation: Nix is required but not installed."));
logging::info(&format!(
"🔄 To use this workflow, please install Nix: https://nixos.org/download.html"
));
logging::info(&format!(
"🔄 Continuing emulation, but nix commands will fail."
));
logging::info("🔄 Emulation: Nix is required but not installed.");
logging::info(
"🔄 To use this workflow, please install Nix: https://nixos.org/download.html",
);
logging::info("🔄 Continuing emulation, but nix commands will fail.");
} else {
logging::info(&format!("🔄 Emulation: Using system-installed Nix"));
logging::info("🔄 Emulation: Using system-installed Nix");
}
Ok(())
} else {
@@ -464,7 +459,7 @@ async fn cleanup_processes() {
// On Unix-like systems, use kill command
let _ = Command::new("kill")
.arg("-TERM")
.arg(&pid.to_string())
.arg(pid.to_string())
.output();
}
@@ -504,7 +499,7 @@ async fn cleanup_workspaces() {
// Only attempt to remove if it exists
if workspace_path.exists() {
match fs::remove_dir_all(&workspace_path) {
Ok(_) => logging::info(&format!("Successfully removed workspace directory")),
Ok(_) => logging::info("Successfully removed workspace directory"),
Err(e) => logging::error(&format!("Error removing workspace: {}", e)),
}
}

362
src/ui.rs
View File

@@ -20,7 +20,6 @@ use ratatui::{
},
Frame, Terminal,
};
use regex;
use std::io::{self, stdout};
use std::path::{Path, PathBuf};
use std::sync::mpsc;
@@ -72,6 +71,9 @@ struct StepExecution {
output: String,
}
// Type alias for the complex execution result type
type ExecutionResultMsg = (usize, Result<(Vec<executor::JobResult>, ()), String>);
// Application state
struct App {
workflows: Vec<Workflow>,
@@ -83,16 +85,16 @@ struct App {
validation_mode: bool,
execution_queue: Vec<usize>, // Indices of workflows to execute
current_execution: Option<usize>,
logs: Vec<String>, // Overall execution logs
log_scroll: usize, // Scrolling position for logs
job_list_state: ListState, // For viewing job details
detailed_view: bool, // Whether we're in detailed view mode
step_list_state: ListState, // For selecting steps in detailed view
step_table_state: TableState, // For the steps table in detailed view
last_tick: Instant, // For UI animations and updates
tick_rate: Duration, // How often to update the UI
tx: mpsc::Sender<(usize, Result<(Vec<executor::JobResult>, ()), String>)>, // Channel for async communication
status_message: Option<String>, // Temporary status message to display
logs: Vec<String>, // Overall execution logs
log_scroll: usize, // Scrolling position for logs
job_list_state: ListState, // For viewing job details
detailed_view: bool, // Whether we're in detailed view mode
step_list_state: ListState, // For selecting steps in detailed view
step_table_state: TableState, // For the steps table in detailed view
last_tick: Instant, // For UI animations and updates
tick_rate: Duration, // How often to update the UI
tx: mpsc::Sender<ExecutionResultMsg>, // Channel for async communication
status_message: Option<String>, // Temporary status message to display
status_message_time: Option<Instant>, // When the message was set
// Search and filter functionality
@@ -153,10 +155,7 @@ impl LogFilterLevel {
}
impl App {
fn new(
runtime_type: RuntimeType,
tx: mpsc::Sender<(usize, Result<(Vec<executor::JobResult>, ()), String>)>,
) -> App {
fn new(runtime_type: RuntimeType, tx: mpsc::Sender<ExecutionResultMsg>) -> App {
let mut workflow_list_state = ListState::default();
workflow_list_state.select(Some(0));
@@ -175,7 +174,7 @@ impl App {
RuntimeType::Docker => {
// Use the safe FD redirection utility from utils
let is_docker_available =
match utils::fd::with_stderr_to_null(|| executor::docker::is_available()) {
match utils::fd::with_stderr_to_null(executor::docker::is_available) {
Ok(result) => result,
Err(_) => {
logging::debug(
@@ -451,15 +450,13 @@ impl App {
// Queue selected workflows for execution
fn queue_selected_for_execution(&mut self) {
if let Some(idx) = self.workflow_list_state.selected() {
if idx < self.workflows.len() {
if !self.execution_queue.contains(&idx) {
self.execution_queue.push(idx);
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Added '{}' to execution queue. Press 'Enter' to start.",
timestamp, self.workflows[idx].name
));
}
if idx < self.workflows.len() && !self.execution_queue.contains(&idx) {
self.execution_queue.push(idx);
let timestamp = Local::now().format("%H:%M:%S").to_string();
self.logs.push(format!(
"[{}] Added '{}' to execution queue. Press 'Enter' to start.",
timestamp, self.workflows[idx].name
));
}
}
}
@@ -1001,26 +998,24 @@ fn load_workflows(dir_path: &Path) -> Vec<Workflow> {
// Default path is .github/workflows
let default_workflows_dir = Path::new(".github").join("workflows");
let is_default_dir = dir_path == &default_workflows_dir || dir_path.ends_with("workflows");
let is_default_dir = dir_path == default_workflows_dir || dir_path.ends_with("workflows");
if let Ok(entries) = std::fs::read_dir(dir_path) {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() && (is_workflow_file(&path) || !is_default_dir) {
let name = path.file_name().map_or_else(
|| "[unknown]".to_string(),
|fname| fname.to_string_lossy().into_owned(),
);
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && (is_workflow_file(&path) || !is_default_dir) {
let name = path.file_name().map_or_else(
|| "[unknown]".to_string(),
|fname| fname.to_string_lossy().into_owned(),
);
workflows.push(Workflow {
name,
path,
selected: false,
status: WorkflowStatus::NotStarted,
execution_details: None,
});
}
workflows.push(Workflow {
name,
path,
selected: false,
status: WorkflowStatus::NotStarted,
execution_details: None,
});
}
}
}
@@ -1071,8 +1066,7 @@ fn render_ui(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App) {
// Render the title bar with tabs
fn render_title_bar(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App, area: Rect) {
// Create tabs
let titles = vec!["Workflows", "Execution", "Logs", "Help"];
let titles = ["Workflows", "Execution", "Logs", "Help"];
let tabs = Tabs::new(
titles
.iter()
@@ -1405,11 +1399,12 @@ fn render_execution_tab(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut A
let error_msg = execution
.logs
.iter()
.filter(|log| log.contains("Error:") || log.contains("Failed"))
.next()
.map_or("Failed to trigger workflow on GitHub.", |s| s.as_str());
.find(|log| log.contains("Error:") || log.contains("Failed"));
(error_msg, None)
(
error_msg.map_or("Failed to trigger workflow on GitHub.", |s| s.as_str()),
None,
)
} else {
("Triggering workflow on GitHub...", None)
};
@@ -2646,7 +2641,7 @@ fn render_status_bar(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App, are
if app.runtime_type == RuntimeType::Docker {
// Check Docker silently using safe FD redirection
let is_docker_available =
match utils::fd::with_stderr_to_null(|| executor::docker::is_available()) {
match utils::fd::with_stderr_to_null(executor::docker::is_available) {
Ok(result) => result,
Err(_) => {
logging::debug("Failed to redirect stderr when checking Docker availability.");
@@ -2763,6 +2758,7 @@ fn render_status_bar(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App, are
}
// Validate a workflow or directory containing workflows
#[allow(clippy::ptr_arg)]
pub fn validate_workflow(path: &PathBuf, verbose: bool) -> io::Result<()> {
let mut workflows = Vec::new();
@@ -2820,134 +2816,8 @@ pub fn validate_workflow(path: &PathBuf, verbose: bool) -> io::Result<()> {
Ok(())
}
pub async fn execute_workflow_cli(
path: &PathBuf,
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::new(
io::ErrorKind::Other,
format!("Error validating workflow: {}", e),
));
}
}
// Check Docker availability if Docker runtime is selected
let runtime_type = match runtime_type {
RuntimeType::Docker => {
if !executor::docker::is_available() {
println!("⚠️ Docker is not available. Using emulation mode instead.");
logging::warning("Docker is not available. Using emulation mode instead.");
RuntimeType::Emulation
} else {
RuntimeType::Docker
}
}
RuntimeType::Emulation => RuntimeType::Emulation,
};
println!("Executing workflow: {}", path.display());
println!("Runtime mode: {:?}", runtime_type);
match executor::execute_workflow(path, runtime_type, verbose).await {
Ok(result) => {
println!("\nWorkflow execution results:");
for job in &result.jobs {
match job.status {
JobStatus::Success => {
println!("\n✅ Job succeeded: {}", job.name);
}
JobStatus::Failure => {
println!("\n❌ Job failed: {}", job.name);
}
JobStatus::Skipped => {
println!("\n⏭️ Job skipped: {}", job.name);
}
}
println!("-------------------------");
for step in job.steps.iter() {
match step.status {
StepStatus::Success => {
println!("{}", step.name);
if !step.output.trim().is_empty() && step.output.lines().count() <= 3 {
// For short outputs, show directly
println!(" {}", step.output.trim());
}
}
StepStatus::Failure => {
println!("{}", step.name);
// For failures, always show output (truncated)
let output = if step.output.len() > 500 {
format!("{}... (truncated)", &step.output[..500])
} else {
step.output.clone()
};
println!(" {}", output.trim().replace('\n', "\n "));
}
StepStatus::Skipped => {
println!(" ⏭️ {} (skipped)", step.name);
}
}
}
}
// Determine overall success
let failures = result
.jobs
.iter()
.filter(|job| job.status == JobStatus::Failure)
.count();
if failures > 0 {
println!("\n❌ Workflow completed with failures");
return Err(io::Error::new(
io::ErrorKind::Other,
"Workflow execution failed",
));
} else {
println!("\n✅ Workflow completed successfully!");
Ok(())
}
}
Err(e) => {
println!("❌ Failed to execute workflow: {}", e);
Err(io::Error::new(
io::ErrorKind::Other,
format!("Workflow execution error: {}", e),
))
}
}
}
// Main entry point for the TUI interface
#[allow(clippy::ptr_arg)]
pub async fn run_wrkflw_tui(
path: Option<&PathBuf>,
runtime_type: RuntimeType,
@@ -2964,8 +2834,8 @@ pub async fn run_wrkflw_tui(
// Set up channel for async communication
let (tx, rx): (
mpsc::Sender<(usize, Result<(Vec<executor::JobResult>, ()), String>)>,
mpsc::Receiver<(usize, Result<(Vec<executor::JobResult>, ()), String>)>,
mpsc::Sender<ExecutionResultMsg>,
mpsc::Receiver<ExecutionResultMsg>,
) = mpsc::channel();
// Initialize app state
@@ -3058,8 +2928,8 @@ pub async fn run_wrkflw_tui(
fn run_tui_event_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
tx_clone: &mpsc::Sender<(usize, Result<(Vec<executor::JobResult>, ()), String>)>,
rx: &mpsc::Receiver<(usize, Result<(Vec<executor::JobResult>, ()), String>)>,
tx_clone: &mpsc::Sender<ExecutionResultMsg>,
rx: &mpsc::Receiver<ExecutionResultMsg>,
verbose: bool,
) -> io::Result<()> {
loop {
@@ -3470,7 +3340,7 @@ async fn execute_curl_trigger(
// Extract common workflow execution logic to avoid duplication
fn start_next_workflow_execution(
app: &mut App,
tx_clone: &mpsc::Sender<(usize, Result<(Vec<executor::JobResult>, ()), String>)>,
tx_clone: &mpsc::Sender<ExecutionResultMsg>,
verbose: bool,
) {
if let Some(next_idx) = app.get_next_workflow_to_execute() {
@@ -3483,7 +3353,7 @@ fn start_next_workflow_execution(
RuntimeType::Docker => {
// Use safe FD redirection to check Docker availability
let is_docker_available =
match utils::fd::with_stderr_to_null(|| executor::docker::is_available()) {
match utils::fd::with_stderr_to_null(executor::docker::is_available) {
Ok(result) => result,
Err(_) => {
logging::debug(
@@ -3606,3 +3476,131 @@ fn start_next_workflow_execution(
logging::info("All workflows completed execution");
}
}
#[allow(clippy::ptr_arg)]
pub async fn execute_workflow_cli(
path: &PathBuf,
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::new(
io::ErrorKind::Other,
format!("Error validating workflow: {}", e),
));
}
}
// Check Docker availability if Docker runtime is selected
let runtime_type = match runtime_type {
RuntimeType::Docker => {
if !executor::docker::is_available() {
println!("⚠️ Docker is not available. Using emulation mode instead.");
logging::warning("Docker is not available. Using emulation mode instead.");
RuntimeType::Emulation
} else {
RuntimeType::Docker
}
}
RuntimeType::Emulation => RuntimeType::Emulation,
};
println!("Executing workflow: {}", path.display());
println!("Runtime mode: {:?}", runtime_type);
match executor::execute_workflow(path, runtime_type, verbose).await {
Ok(result) => {
println!("\nWorkflow execution results:");
for job in &result.jobs {
match job.status {
JobStatus::Success => {
println!("\n✅ Job succeeded: {}", job.name);
}
JobStatus::Failure => {
println!("\n❌ Job failed: {}", job.name);
}
JobStatus::Skipped => {
println!("\n⏭️ Job skipped: {}", job.name);
}
}
println!("-------------------------");
for step in job.steps.iter() {
match step.status {
StepStatus::Success => {
println!("{}", step.name);
if !step.output.trim().is_empty() && step.output.lines().count() <= 3 {
// For short outputs, show directly
println!(" {}", step.output.trim());
}
}
StepStatus::Failure => {
println!("{}", step.name);
// For failures, always show output (truncated)
let output = if step.output.len() > 500 {
format!("{}... (truncated)", &step.output[..500])
} else {
step.output.clone()
};
println!(" {}", output.trim().replace('\n', "\n "));
}
StepStatus::Skipped => {
println!(" ⏭️ {} (skipped)", step.name);
}
}
}
}
// Determine overall success
let failures = result
.jobs
.iter()
.filter(|job| job.status == JobStatus::Failure)
.count();
if failures > 0 {
println!("\n❌ Workflow completed with failures");
Err(io::Error::new(
io::ErrorKind::Other,
"Workflow execution failed",
))
} else {
println!("\n✅ Workflow completed successfully!");
Ok(())
}
}
Err(e) => {
println!("❌ Failed to execute workflow: {}", e);
Err(io::Error::new(
io::ErrorKind::Other,
format!("Workflow execution error: {}", e),
))
}
}
}

View File

@@ -13,12 +13,12 @@ pub fn validate_jobs(jobs: &Value, result: &mut ValidationResult) {
if let Some(job_name) = job_name.as_str() {
if let Some(job_config) = job_config.as_mapping() {
// Check for required 'runs-on'
if !job_config.contains_key(&Value::String("runs-on".to_string())) {
if !job_config.contains_key(Value::String("runs-on".to_string())) {
result.add_issue(format!("Job '{}' is missing 'runs-on' field", job_name));
}
// Check for steps
match job_config.get(&Value::String("steps".to_string())) {
match job_config.get(Value::String("steps".to_string())) {
Some(Value::Sequence(steps)) => {
if steps.is_empty() {
result.add_issue(format!(
@@ -45,11 +45,11 @@ pub fn validate_jobs(jobs: &Value, result: &mut ValidationResult) {
// Check for job dependencies
if let Some(Value::Sequence(needs)) =
job_config.get(&Value::String("needs".to_string()))
job_config.get(Value::String("needs".to_string()))
{
for need in needs {
if let Some(need_str) = need.as_str() {
if !jobs_map.contains_key(&Value::String(need_str.to_string())) {
if !jobs_map.contains_key(Value::String(need_str.to_string())) {
result.add_issue(format!(
"Job '{}' depends on non-existent job '{}'",
job_name, need_str
@@ -58,9 +58,9 @@ pub fn validate_jobs(jobs: &Value, result: &mut ValidationResult) {
}
}
} else if let Some(Value::String(need)) =
job_config.get(&Value::String("needs".to_string()))
job_config.get(Value::String("needs".to_string()))
{
if !jobs_map.contains_key(&Value::String(need.clone())) {
if !jobs_map.contains_key(Value::String(need.clone())) {
result.add_issue(format!(
"Job '{}' depends on non-existent job '{}'",
job_name, need
@@ -69,7 +69,7 @@ pub fn validate_jobs(jobs: &Value, result: &mut ValidationResult) {
}
// Validate matrix configuration if present
if let Some(matrix) = job_config.get(&Value::String("matrix".to_string())) {
if let Some(matrix) = job_config.get(Value::String("matrix".to_string())) {
validate_matrix(matrix, result);
}
} else {

View File

@@ -2,12 +2,12 @@ use crate::models::ValidationResult;
use crate::validators::validate_action_reference;
use serde_yaml::Value;
pub fn validate_steps(steps: &Vec<Value>, job_name: &str, result: &mut ValidationResult) {
pub fn validate_steps(steps: &[Value], job_name: &str, result: &mut ValidationResult) {
for (i, step) in steps.iter().enumerate() {
if let Some(step_map) = step.as_mapping() {
if !step_map.contains_key(&Value::String("name".to_string()))
&& !step_map.contains_key(&Value::String("uses".to_string()))
&& !step_map.contains_key(&Value::String("run".to_string()))
if !step_map.contains_key(Value::String("name".to_string()))
&& !step_map.contains_key(Value::String("uses".to_string()))
&& !step_map.contains_key(Value::String("run".to_string()))
{
result.add_issue(format!(
"Job '{}', step {}: Missing 'name', 'uses', or 'run' field",
@@ -17,8 +17,8 @@ pub fn validate_steps(steps: &Vec<Value>, job_name: &str, result: &mut Validatio
}
// Check for both 'uses' and 'run' in the same step
if step_map.contains_key(&Value::String("uses".to_string()))
&& step_map.contains_key(&Value::String("run".to_string()))
if step_map.contains_key(Value::String("uses".to_string()))
&& step_map.contains_key(Value::String("run".to_string()))
{
result.add_issue(format!(
"Job '{}', step {}: Contains both 'uses' and 'run' (should only use one)",
@@ -28,7 +28,7 @@ pub fn validate_steps(steps: &Vec<Value>, job_name: &str, result: &mut Validatio
}
// Validate action reference if 'uses' is present
if let Some(Value::String(uses)) = step_map.get(&Value::String("uses".to_string())) {
if let Some(Value::String(uses)) = step_map.get(Value::String("uses".to_string())) {
validate_action_reference(uses, job_name, i, result);
}
} else {

View File

@@ -63,12 +63,12 @@ pub fn validate_triggers(on: &Value, result: &mut ValidationResult) {
// Check schedule syntax if present
if let Some(Value::Sequence(schedules)) =
event_map.get(&Value::String("schedule".to_string()))
event_map.get(Value::String("schedule".to_string()))
{
for schedule in schedules {
if let Some(schedule_map) = schedule.as_mapping() {
if let Some(Value::String(cron)) =
schedule_map.get(&Value::String("cron".to_string()))
schedule_map.get(Value::String("cron".to_string()))
{
validate_cron_syntax(cron, result);
} else {