mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2026-05-18 13:16:04 +02:00
make clippy happy
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
362
src/ui.rs
@@ -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),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user