Merge pull request #6 from bahdotsh/bahdotsh/local_composite_actions

fix: local composite actions
This commit is contained in:
Gokul
2025-04-05 09:18:27 +05:30
committed by GitHub
6 changed files with 1338 additions and 339 deletions

105
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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]

View File

@@ -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,
})
}

View File

@@ -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);
}

1309
src/ui.rs

File diff suppressed because it is too large Load Diff

View File

@@ -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
));
}
}
}