mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2026-02-23 19:39:42 +01:00
fix: local composite actions
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -356,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
|
||||
@@ -615,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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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