documenting action spec

This commit is contained in:
2026-02-09 00:30:48 -06:00
parent a74e13fa0b
commit 588b319fec
38 changed files with 3050 additions and 286 deletions

View File

@@ -4,7 +4,8 @@
use super::{
parameter_passing::{self, ParameterDeliveryConfig},
BoundedLogWriter, ExecutionContext, ExecutionResult, Runtime, RuntimeError, RuntimeResult,
BoundedLogWriter, ExecutionContext, ExecutionResult, OutputFormat, Runtime, RuntimeError,
RuntimeResult,
};
use async_trait::async_trait;
use std::path::PathBuf;
@@ -58,6 +59,7 @@ impl ShellRuntime {
timeout_secs: Option<u64>,
max_stdout_bytes: usize,
max_stderr_bytes: usize,
output_format: OutputFormat,
) -> RuntimeResult<ExecutionResult> {
let start = Instant::now();
@@ -198,14 +200,41 @@ impl ShellRuntime {
exit_code, duration_ms, stdout_result.truncated, stderr_result.truncated
);
// Try to parse result from stdout as JSON
// Parse result from stdout based on output_format
let result = if exit_code == 0 && !stdout_result.content.trim().is_empty() {
stdout_result
.content
.trim()
.lines()
.last()
.and_then(|line| serde_json::from_str(line).ok())
match output_format {
OutputFormat::Text => {
// No parsing - text output is captured in stdout field
None
}
OutputFormat::Json => {
// Try to parse last line of stdout as JSON
stdout_result
.content
.trim()
.lines()
.last()
.and_then(|line| serde_json::from_str(line).ok())
}
OutputFormat::Yaml => {
// Try to parse stdout as YAML
serde_yaml_ng::from_str(stdout_result.content.trim()).ok()
}
OutputFormat::Jsonl => {
// Parse each line as JSON and collect into array
let mut items = Vec::new();
for line in stdout_result.content.trim().lines() {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(line) {
items.push(value);
}
}
if items.is_empty() {
None
} else {
Some(serde_json::Value::Array(items))
}
}
}
} else {
None
};
@@ -338,6 +367,7 @@ impl ShellRuntime {
timeout_secs: Option<u64>,
max_stdout_bytes: usize,
max_stderr_bytes: usize,
output_format: OutputFormat,
) -> RuntimeResult<ExecutionResult> {
debug!(
"Executing shell script with {} secrets (passed via stdin)",
@@ -360,6 +390,7 @@ impl ShellRuntime {
timeout_secs,
max_stdout_bytes,
max_stderr_bytes,
output_format,
)
.await
}
@@ -374,6 +405,7 @@ impl ShellRuntime {
timeout_secs: Option<u64>,
max_stdout_bytes: usize,
max_stderr_bytes: usize,
output_format: OutputFormat,
) -> RuntimeResult<ExecutionResult> {
debug!(
"Executing shell file: {:?} with {} secrets",
@@ -397,6 +429,7 @@ impl ShellRuntime {
timeout_secs,
max_stdout_bytes,
max_stderr_bytes,
output_format,
)
.await
}
@@ -448,11 +481,8 @@ impl Runtime for ShellRuntime {
format: context.parameter_format,
};
let prepared_params = parameter_passing::prepare_parameters(
&context.parameters,
&mut env,
config,
)?;
let prepared_params =
parameter_passing::prepare_parameters(&context.parameters, &mut env, config)?;
// Get stdin content if parameters are delivered via stdin
let parameters_stdin = prepared_params.stdin_content();
@@ -478,6 +508,7 @@ impl Runtime for ShellRuntime {
context.timeout,
context.max_stdout_bytes,
context.max_stderr_bytes,
context.output_format,
)
.await;
}
@@ -492,6 +523,7 @@ impl Runtime for ShellRuntime {
context.timeout,
context.max_stdout_bytes,
context.max_stderr_bytes,
context.output_format,
)
.await
}
@@ -577,6 +609,7 @@ mod tests {
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
output_format: attune_common::models::OutputFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -609,6 +642,7 @@ mod tests {
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
output_format: attune_common::models::OutputFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -636,6 +670,7 @@ mod tests {
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
output_format: attune_common::models::OutputFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -665,6 +700,7 @@ mod tests {
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
output_format: attune_common::models::OutputFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -709,6 +745,7 @@ echo "missing=$missing"
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
output_format: attune_common::models::OutputFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -720,4 +757,58 @@ echo "missing=$missing"
assert!(result.stdout.contains("db_pass=super_secret_pass"));
assert!(result.stdout.contains("missing="));
}
#[tokio::test]
async fn test_shell_runtime_jsonl_output() {
let runtime = ShellRuntime::new();
let context = ExecutionContext {
execution_id: 6,
action_ref: "test.jsonl".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(10),
working_dir: None,
entry_point: "shell".to_string(),
code: Some(
r#"
echo '{"id": 1, "name": "Alice"}'
echo '{"id": 2, "name": "Bob"}'
echo '{"id": 3, "name": "Charlie"}'
"#
.to_string(),
),
code_path: None,
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
output_format: attune_common::models::OutputFormat::Jsonl,
};
let result = runtime.execute(context).await.unwrap();
assert!(result.is_success());
assert_eq!(result.exit_code, 0);
// Verify result is parsed as an array of JSON objects
let parsed_result = result.result.expect("Should have parsed result");
assert!(parsed_result.is_array());
let items = parsed_result.as_array().unwrap();
assert_eq!(items.len(), 3);
// Verify first item
assert_eq!(items[0]["id"], 1);
assert_eq!(items[0]["name"], "Alice");
// Verify second item
assert_eq!(items[1]["id"], 2);
assert_eq!(items[1]["name"], "Bob");
// Verify third item
assert_eq!(items[2]["id"], 3);
assert_eq!(items[2]["name"], "Charlie");
}
}