mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2025-12-29 00:24:57 +01:00
Merge pull request #6 from bahdotsh/bahdotsh/local_composite_actions
fix: local composite actions
This commit is contained in:
105
Cargo.lock
generated
105
Cargo.lock
generated
@@ -253,7 +253,7 @@ version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -289,9 +289,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
@@ -303,6 +303,22 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
@@ -558,6 +574,12 @@ version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -834,12 +856,27 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
@@ -1010,6 +1047,12 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@@ -1078,6 +1121,23 @@ version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e2e4cd95294a85c3b4446e63ef054eea43e0205b1fd60120c16b74ff7ff96ad"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cassowary",
|
||||
"crossterm 0.27.0",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.10"
|
||||
@@ -1304,6 +1364,28 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.25.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
@@ -1484,19 +1566,6 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tui"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cassowary",
|
||||
"crossterm",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@@ -1868,19 +1937,19 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored",
|
||||
"crossterm",
|
||||
"crossterm 0.26.1",
|
||||
"dirs",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tui",
|
||||
"uuid",
|
||||
"which",
|
||||
]
|
||||
|
||||
@@ -28,8 +28,8 @@ dirs = "5.0"
|
||||
thiserror = "1.0"
|
||||
log = "0.4"
|
||||
which = "4.4"
|
||||
crossterm = "0.25"
|
||||
tui = "0.19"
|
||||
crossterm = "0.26.1"
|
||||
ratatui = { version = "0.23.0", features = ["crossterm"] }
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -79,6 +80,7 @@ pub struct JobResult {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum JobStatus {
|
||||
Success,
|
||||
Failure,
|
||||
@@ -355,9 +357,27 @@ async fn execute_step(
|
||||
output,
|
||||
})
|
||||
} else {
|
||||
// Other actions - original code for handling non-checkout actions
|
||||
// Get action info
|
||||
let image = prepare_action(&action_info, runtime).await?;
|
||||
|
||||
// Special handling for composite actions
|
||||
if image == "composite" && action_info.is_local {
|
||||
// Handle composite action
|
||||
let action_path = Path::new(&action_info.repository);
|
||||
return execute_composite_action(
|
||||
step,
|
||||
action_path,
|
||||
&step_env,
|
||||
working_dir,
|
||||
runtime,
|
||||
job_runs_on,
|
||||
verbose,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Regular Docker or JavaScript action processing
|
||||
// ... (rest of the existing code for handling regular actions)
|
||||
// Build command for Docker action
|
||||
let mut cmd = Vec::new();
|
||||
let mut owned_strings = Vec::new(); // Keep strings alive until after we use cmd
|
||||
@@ -614,3 +634,208 @@ async fn prepare_runner_image(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_composite_action(
|
||||
step: &crate::parser::workflow::Step,
|
||||
action_path: &Path,
|
||||
job_env: &HashMap<String, String>,
|
||||
working_dir: &Path,
|
||||
runtime: &Box<dyn ContainerRuntime>,
|
||||
job_runs_on: &str,
|
||||
verbose: bool,
|
||||
) -> Result<StepResult, ExecutionError> {
|
||||
// Find the action definition file
|
||||
let action_yaml = action_path.join("action.yml");
|
||||
let action_yaml_alt = action_path.join("action.yaml");
|
||||
|
||||
let action_file = if action_yaml.exists() {
|
||||
action_yaml
|
||||
} else if action_yaml_alt.exists() {
|
||||
action_yaml_alt
|
||||
} else {
|
||||
return Err(ExecutionError::ExecutionError(format!(
|
||||
"No action.yml or action.yaml found in {}",
|
||||
action_path.display()
|
||||
)));
|
||||
};
|
||||
|
||||
// Parse the composite action definition
|
||||
let action_content = fs::read_to_string(&action_file).map_err(|e| {
|
||||
ExecutionError::ExecutionError(format!("Failed to read action file: {}", e))
|
||||
})?;
|
||||
|
||||
let action_def: serde_yaml::Value = serde_yaml::from_str(&action_content)
|
||||
.map_err(|e| ExecutionError::ExecutionError(format!("Invalid action YAML: {}", e)))?;
|
||||
|
||||
// Check if it's a composite action
|
||||
match action_def.get("runs").and_then(|v| v.get("using")) {
|
||||
Some(serde_yaml::Value::String(using)) if using == "composite" => {
|
||||
// Get the steps
|
||||
let steps = match action_def.get("runs").and_then(|v| v.get("steps")) {
|
||||
Some(serde_yaml::Value::Sequence(steps)) => steps,
|
||||
_ => {
|
||||
return Err(ExecutionError::ExecutionError(
|
||||
"Composite action is missing steps".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Process inputs from the calling step's 'with' parameters
|
||||
let mut action_env = job_env.clone();
|
||||
if let Some(inputs_def) = action_def.get("inputs") {
|
||||
if let Some(inputs_map) = inputs_def.as_mapping() {
|
||||
for (input_name, input_def) in inputs_map {
|
||||
if let Some(input_name_str) = input_name.as_str() {
|
||||
// Get default value if available
|
||||
let default_value = input_def
|
||||
.get("default")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// Check if the input was provided in the 'with' section
|
||||
let input_value = step
|
||||
.with
|
||||
.as_ref()
|
||||
.and_then(|with| with.get(input_name_str))
|
||||
.unwrap_or(&default_value.to_string())
|
||||
.clone();
|
||||
|
||||
// Add to environment as INPUT_X
|
||||
action_env.insert(
|
||||
format!("INPUT_{}", input_name_str.to_uppercase()),
|
||||
input_value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute each step
|
||||
let mut step_outputs = Vec::new();
|
||||
for (idx, step_def) in steps.iter().enumerate() {
|
||||
// Convert the YAML step to our Step struct
|
||||
let composite_step = match convert_yaml_to_step(step_def) {
|
||||
Ok(step) => step,
|
||||
Err(e) => {
|
||||
return Err(ExecutionError::ExecutionError(format!(
|
||||
"Failed to process composite action step {}: {}",
|
||||
idx + 1,
|
||||
e
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Execute the step - using Box::pin to handle async recursion
|
||||
let step_result = Box::pin(execute_step(
|
||||
&composite_step,
|
||||
idx,
|
||||
&action_env,
|
||||
working_dir,
|
||||
runtime,
|
||||
&crate::parser::workflow::WorkflowDefinition {
|
||||
name: "Composite Action".to_string(),
|
||||
on: vec![],
|
||||
on_raw: serde_yaml::Value::Null,
|
||||
jobs: HashMap::new(),
|
||||
},
|
||||
job_runs_on,
|
||||
verbose,
|
||||
))
|
||||
.await?;
|
||||
|
||||
// Add output to results
|
||||
step_outputs.push(format!("Step {}: {}", idx + 1, step_result.output));
|
||||
|
||||
// Short-circuit on failure if needed
|
||||
if step_result.status == StepStatus::Failure {
|
||||
return Ok(StepResult {
|
||||
name: step
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Composite Action".to_string()),
|
||||
status: StepStatus::Failure,
|
||||
output: format!("Composite action failed:\n{}", step_outputs.join("\n")),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// All steps completed successfully
|
||||
Ok(StepResult {
|
||||
name: step
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Composite Action".to_string()),
|
||||
status: StepStatus::Success,
|
||||
output: format!("Composite action completed:\n{}", step_outputs.join("\n")),
|
||||
})
|
||||
}
|
||||
_ => Err(ExecutionError::ExecutionError(
|
||||
"Action is not a composite action or has invalid format".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
// Extract step properties
|
||||
let name = step_yaml
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let uses = step_yaml
|
||||
.get("uses")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let run = step_yaml
|
||||
.get("run")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let shell = step_yaml
|
||||
.get("shell")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let with = step_yaml.get("with").and_then(|v| v.as_mapping()).map(|m| {
|
||||
let mut with_map = HashMap::new();
|
||||
for (k, v) in m {
|
||||
if let (Some(key), Some(value)) = (k.as_str(), v.as_str()) {
|
||||
with_map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
with_map
|
||||
});
|
||||
|
||||
let env = step_yaml
|
||||
.get("env")
|
||||
.and_then(|v| v.as_mapping())
|
||||
.map(|m| {
|
||||
let mut env_map = HashMap::new();
|
||||
for (k, v) in m {
|
||||
if let (Some(key), Some(value)) = (k.as_str(), v.as_str()) {
|
||||
env_map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
env_map
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// For composite steps with shell, construct a run step
|
||||
let final_run = if shell.is_some() && run.is_some() {
|
||||
run
|
||||
} else {
|
||||
run
|
||||
};
|
||||
|
||||
Ok(crate::parser::workflow::Step {
|
||||
name,
|
||||
uses,
|
||||
run: final_run,
|
||||
with: with,
|
||||
env,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ pub fn get_logs() -> Vec<String> {
|
||||
}
|
||||
|
||||
// Clear all logs
|
||||
#[allow(dead_code)]
|
||||
pub fn clear_logs() {
|
||||
if let Ok(mut logs) = LOGS.lock() {
|
||||
logs.clear();
|
||||
@@ -56,6 +57,7 @@ pub fn clear_logs() {
|
||||
}
|
||||
|
||||
// Convenience functions for different log levels
|
||||
#[allow(dead_code)]
|
||||
pub fn debug(message: &str) {
|
||||
log(LogLevel::Debug, message);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ pub fn validate_action_reference(
|
||||
step_idx: usize,
|
||||
result: &mut ValidationResult,
|
||||
) {
|
||||
// Check for valid action reference formats
|
||||
if !action_ref.contains('/') && !action_ref.contains('.') {
|
||||
// Check if it's a local action (starts with ./)
|
||||
let is_local_action = action_ref.starts_with("./");
|
||||
|
||||
// For non-local actions, enforce standard format
|
||||
if !is_local_action && !action_ref.contains('/') && !action_ref.contains('.') {
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Invalid action reference format '{}'",
|
||||
job_name,
|
||||
@@ -17,8 +20,8 @@ pub fn validate_action_reference(
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for version tag or commit SHA
|
||||
if action_ref.contains('@') {
|
||||
// Check for version tag or commit SHA, but only for non-local actions
|
||||
if !is_local_action && action_ref.contains('@') {
|
||||
let parts: Vec<&str> = action_ref.split('@').collect();
|
||||
if parts.len() != 2 || parts[1].is_empty() {
|
||||
result.add_issue(format!(
|
||||
@@ -28,8 +31,8 @@ pub fn validate_action_reference(
|
||||
action_ref
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// Missing version tag is not recommended
|
||||
} else if !is_local_action {
|
||||
// Missing version tag is not recommended for non-local actions
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Action '{}' is missing version tag (@v2, @main, etc.)",
|
||||
job_name,
|
||||
@@ -37,4 +40,19 @@ pub fn validate_action_reference(
|
||||
action_ref
|
||||
));
|
||||
}
|
||||
|
||||
// For local actions, verify the path exists
|
||||
if is_local_action {
|
||||
let action_path = std::path::Path::new(action_ref);
|
||||
if !action_path.exists() {
|
||||
// We can't reliably check this during validation since the working directory
|
||||
// might not be the repository root, but we'll add a warning
|
||||
result.add_issue(format!(
|
||||
"Job '{}', step {}: Local action path '{}' may not exist at runtime",
|
||||
job_name,
|
||||
step_idx + 1,
|
||||
action_ref
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user