[wip] cli capability parity
Some checks failed
CI / Rustfmt (push) Successful in 23s
CI / Cargo Audit & Deny (push) Successful in 30s
CI / Web Blocking Checks (push) Successful in 48s
CI / Security Blocking Checks (push) Successful in 8s
CI / Clippy (push) Failing after 1m55s
CI / Web Advisory Checks (push) Successful in 35s
CI / Security Advisory Checks (push) Successful in 37s
CI / Tests (push) Successful in 8m5s
Some checks failed
CI / Rustfmt (push) Successful in 23s
CI / Cargo Audit & Deny (push) Successful in 30s
CI / Web Blocking Checks (push) Successful in 48s
CI / Security Blocking Checks (push) Successful in 8s
CI / Clippy (push) Failing after 1m55s
CI / Web Advisory Checks (push) Successful in 35s
CI / Security Advisory Checks (push) Successful in 37s
CI / Tests (push) Successful in 8m5s
This commit is contained in:
@@ -105,8 +105,9 @@ pub struct ExecutionContext {
|
||||
/// Environment variables
|
||||
pub env: HashMap<String, String>,
|
||||
|
||||
/// Secrets (passed securely via stdin, not environment variables)
|
||||
pub secrets: HashMap<String, String>,
|
||||
/// Secrets (passed securely via stdin, not environment variables).
|
||||
/// Values are JSON — strings, objects, arrays, numbers, or booleans.
|
||||
pub secrets: HashMap<String, serde_json::Value>,
|
||||
|
||||
/// Execution timeout in seconds
|
||||
pub timeout: Option<u64>,
|
||||
|
||||
@@ -39,7 +39,7 @@ impl NativeRuntime {
|
||||
async fn execute_binary(
|
||||
&self,
|
||||
binary_path: PathBuf,
|
||||
secrets: &std::collections::HashMap<String, String>,
|
||||
_secrets: &std::collections::HashMap<String, serde_json::Value>,
|
||||
env: &std::collections::HashMap<String, String>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout: Option<u64>,
|
||||
@@ -94,31 +94,17 @@ impl NativeRuntime {
|
||||
.spawn()
|
||||
.map_err(|e| RuntimeError::ExecutionFailed(format!("Failed to spawn binary: {}", e)))?;
|
||||
|
||||
// Write to stdin - parameters (if using stdin delivery) and/or secrets
|
||||
// If this fails, the process has already started, so we continue and capture output
|
||||
// Write parameters to stdin as a single JSON line.
|
||||
// Secrets are merged into the parameters map by the caller, so the
|
||||
// action reads everything with a single readline().
|
||||
let stdin_write_error = if let Some(mut stdin) = child.stdin.take() {
|
||||
let mut error = None;
|
||||
|
||||
// Write parameters first if using stdin delivery
|
||||
if let Some(params_data) = parameters_stdin {
|
||||
if let Err(e) = stdin.write_all(params_data.as_bytes()).await {
|
||||
error = Some(format!("Failed to write parameters to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n---ATTUNE_PARAMS_END---\n").await {
|
||||
error = Some(format!("Failed to write parameter delimiter: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// Write secrets as JSON (always, for backward compatibility)
|
||||
if error.is_none() && !secrets.is_empty() {
|
||||
match serde_json::to_string(secrets) {
|
||||
Ok(secrets_json) => {
|
||||
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
|
||||
error = Some(format!("Failed to write secrets to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
error = Some(format!("Failed to write newline to stdin: {}", e));
|
||||
}
|
||||
}
|
||||
Err(e) => error = Some(format!("Failed to serialize secrets: {}", e)),
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
error = Some(format!("Failed to write newline to stdin: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +317,15 @@ impl Runtime for NativeRuntime {
|
||||
context.action_ref, context.execution_id, context.parameter_delivery, context.parameter_format
|
||||
);
|
||||
|
||||
// Merge secrets into parameters as a single JSON document.
|
||||
// Actions receive everything via one readline() on stdin.
|
||||
// Secret values are already JsonValue (string, object, array, etc.)
|
||||
// so they are inserted directly without wrapping.
|
||||
let mut merged_parameters = context.parameters.clone();
|
||||
for (key, value) in &context.secrets {
|
||||
merged_parameters.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
// Prepare environment and parameters according to delivery method
|
||||
let mut env = context.env.clone();
|
||||
let config = ParameterDeliveryConfig {
|
||||
@@ -339,7 +334,7 @@ impl Runtime for NativeRuntime {
|
||||
};
|
||||
|
||||
let prepared_params =
|
||||
parameter_passing::prepare_parameters(&context.parameters, &mut env, config)?;
|
||||
parameter_passing::prepare_parameters(&merged_parameters, &mut env, config)?;
|
||||
|
||||
// Get stdin content if parameters are delivered via stdin
|
||||
let parameters_stdin = prepared_params.stdin_content();
|
||||
@@ -351,7 +346,7 @@ impl Runtime for NativeRuntime {
|
||||
|
||||
self.execute_binary(
|
||||
binary_path,
|
||||
&context.secrets,
|
||||
&std::collections::HashMap::new(),
|
||||
&env,
|
||||
parameters_stdin,
|
||||
context.timeout,
|
||||
|
||||
@@ -20,6 +20,7 @@ use super::{
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use attune_common::models::runtime::{EnvironmentConfig, RuntimeExecutionConfig};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::process::Command;
|
||||
use tracing::{debug, error, info, warn};
|
||||
@@ -645,12 +646,21 @@ impl Runtime for ProcessRuntime {
|
||||
env.insert(key.clone(), resolved);
|
||||
}
|
||||
}
|
||||
// Merge secrets into parameters as a single JSON document.
|
||||
// Actions receive everything via one readline() on stdin.
|
||||
// Secret values are already JsonValue (string, object, array, etc.)
|
||||
// so they are inserted directly without wrapping.
|
||||
let mut merged_parameters = context.parameters.clone();
|
||||
for (key, value) in &context.secrets {
|
||||
merged_parameters.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
let param_config = ParameterDeliveryConfig {
|
||||
delivery: context.parameter_delivery,
|
||||
format: context.parameter_format,
|
||||
};
|
||||
let prepared_params =
|
||||
parameter_passing::prepare_parameters(&context.parameters, &mut env, param_config)?;
|
||||
parameter_passing::prepare_parameters(&merged_parameters, &mut env, param_config)?;
|
||||
let parameters_stdin = prepared_params.stdin_content();
|
||||
|
||||
// Determine working directory: use context override, or pack dir
|
||||
@@ -725,10 +735,11 @@ impl Runtime for ProcessRuntime {
|
||||
.unwrap_or_else(|| "<none>".to_string()),
|
||||
);
|
||||
|
||||
// Execute with streaming output capture (with optional cancellation support)
|
||||
// Execute with streaming output capture (with optional cancellation support).
|
||||
// Secrets are already merged into parameters — no separate secrets arg needed.
|
||||
process_executor::execute_streaming_cancellable(
|
||||
cmd,
|
||||
&context.secrets,
|
||||
&HashMap::new(),
|
||||
parameters_stdin,
|
||||
context.timeout,
|
||||
context.max_stdout_bytes,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Provides common subprocess execution infrastructure used by all runtime
|
||||
//! implementations. Handles streaming stdout/stderr capture, bounded log
|
||||
//! collection, timeout management, stdin parameter/secret delivery, and
|
||||
//! collection, timeout management, stdin parameter delivery, and
|
||||
//! output format parsing.
|
||||
//!
|
||||
//! ## Cancellation Support
|
||||
@@ -28,22 +28,22 @@ use tracing::{debug, info, warn};
|
||||
/// This is the core execution function used by all runtime implementations.
|
||||
/// It handles:
|
||||
/// - Spawning the process with piped I/O
|
||||
/// - Writing parameters and secrets to stdin
|
||||
/// - Writing parameters (with secrets merged in) to stdin
|
||||
/// - Streaming stdout/stderr with bounded log collection
|
||||
/// - Timeout management
|
||||
/// - Output format parsing (JSON, YAML, JSONL, text)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cmd` - Pre-configured `Command` (interpreter, args, env vars, working dir already set)
|
||||
/// * `secrets` - Secrets to pass via stdin (as JSON)
|
||||
/// * `parameters_stdin` - Optional parameter data to write to stdin before secrets
|
||||
/// * `secrets` - Deprecated/unused — secrets are now merged into parameters by the caller
|
||||
/// * `parameters_stdin` - Optional parameter data (including secrets) to write to stdin
|
||||
/// * `timeout_secs` - Optional execution timeout in seconds
|
||||
/// * `max_stdout_bytes` - Maximum stdout size before truncation
|
||||
/// * `max_stderr_bytes` - Maximum stderr size before truncation
|
||||
/// * `output_format` - How to parse stdout (Text, Json, Yaml, Jsonl)
|
||||
pub async fn execute_streaming(
|
||||
cmd: Command,
|
||||
secrets: &HashMap<String, String>,
|
||||
_secrets: &HashMap<String, serde_json::Value>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout_secs: Option<u64>,
|
||||
max_stdout_bytes: usize,
|
||||
@@ -52,7 +52,7 @@ pub async fn execute_streaming(
|
||||
) -> RuntimeResult<ExecutionResult> {
|
||||
execute_streaming_cancellable(
|
||||
cmd,
|
||||
secrets,
|
||||
_secrets,
|
||||
parameters_stdin,
|
||||
timeout_secs,
|
||||
max_stdout_bytes,
|
||||
@@ -68,7 +68,7 @@ pub async fn execute_streaming(
|
||||
/// This is the core execution function used by all runtime implementations.
|
||||
/// It handles:
|
||||
/// - Spawning the process with piped I/O
|
||||
/// - Writing parameters and secrets to stdin
|
||||
/// - Writing parameters (with secrets merged in) to stdin
|
||||
/// - Streaming stdout/stderr with bounded log collection
|
||||
/// - Timeout management
|
||||
/// - Graceful cancellation via SIGINT → SIGTERM → SIGKILL escalation
|
||||
@@ -76,8 +76,8 @@ pub async fn execute_streaming(
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cmd` - Pre-configured `Command` (interpreter, args, env vars, working dir already set)
|
||||
/// * `secrets` - Secrets to pass via stdin (as JSON)
|
||||
/// * `parameters_stdin` - Optional parameter data to write to stdin before secrets
|
||||
/// * `secrets` - Deprecated/unused — secrets are now merged into parameters by the caller
|
||||
/// * `parameters_stdin` - Optional parameter data (including secrets) to write to stdin
|
||||
/// * `timeout_secs` - Optional execution timeout in seconds
|
||||
/// * `max_stdout_bytes` - Maximum stdout size before truncation
|
||||
/// * `max_stderr_bytes` - Maximum stderr size before truncation
|
||||
@@ -86,7 +86,7 @@ pub async fn execute_streaming(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_streaming_cancellable(
|
||||
mut cmd: Command,
|
||||
secrets: &HashMap<String, String>,
|
||||
_secrets: &HashMap<String, serde_json::Value>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout_secs: Option<u64>,
|
||||
max_stdout_bytes: usize,
|
||||
@@ -103,34 +103,19 @@ pub async fn execute_streaming_cancellable(
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
// Write to stdin - parameters (if using stdin delivery) and/or secrets.
|
||||
// Write to stdin - parameters (with secrets already merged in by the caller).
|
||||
// If this fails, the process has already started, so we continue and capture output.
|
||||
let stdin_write_error = if let Some(mut stdin) = child.stdin.take() {
|
||||
let mut error = None;
|
||||
|
||||
// Write parameters first if using stdin delivery.
|
||||
// When the caller provides parameters_stdin (i.e. the action uses
|
||||
// stdin delivery), always write the content — even if it's "{}" —
|
||||
// because the script expects to read valid JSON from stdin.
|
||||
// Write parameters to stdin as a single JSON line.
|
||||
// Secrets are merged into the parameters map by the caller, so the
|
||||
// action reads everything with a single readline().
|
||||
if let Some(params_data) = parameters_stdin {
|
||||
if let Err(e) = stdin.write_all(params_data.as_bytes()).await {
|
||||
error = Some(format!("Failed to write parameters to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n---ATTUNE_PARAMS_END---\n").await {
|
||||
error = Some(format!("Failed to write parameter delimiter: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// Write secrets as JSON (always, for backward compatibility)
|
||||
if error.is_none() && !secrets.is_empty() {
|
||||
match serde_json::to_string(secrets) {
|
||||
Ok(secrets_json) => {
|
||||
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
|
||||
error = Some(format!("Failed to write secrets to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
error = Some(format!("Failed to write newline to stdin: {}", e));
|
||||
}
|
||||
}
|
||||
Err(e) => error = Some(format!("Failed to serialize secrets: {}", e)),
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
error = Some(format!("Failed to write newline to stdin: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ impl ShellRuntime {
|
||||
async fn execute_with_streaming(
|
||||
&self,
|
||||
mut cmd: Command,
|
||||
secrets: &std::collections::HashMap<String, String>,
|
||||
_secrets: &std::collections::HashMap<String, String>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout_secs: Option<u64>,
|
||||
max_stdout_bytes: usize,
|
||||
@@ -81,39 +81,19 @@ impl ShellRuntime {
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
// Write to stdin - parameters (if using stdin delivery) and/or secrets
|
||||
// If this fails, the process has already started, so we continue and capture output
|
||||
// Write to stdin - parameters (with secrets already merged in by the caller).
|
||||
// If this fails, the process has already started, so we continue and capture output.
|
||||
let stdin_write_error = if let Some(mut stdin) = child.stdin.take() {
|
||||
let mut error = None;
|
||||
|
||||
// Write parameters first if using stdin delivery.
|
||||
// Skip empty/trivial content ("{}","","[]") to avoid polluting stdin
|
||||
// before secrets — scripts that read secrets via readline() expect
|
||||
// the secrets JSON as the first line.
|
||||
let has_real_params = parameters_stdin
|
||||
.map(|s| !matches!(s.trim(), "" | "{}" | "[]"))
|
||||
.unwrap_or(false);
|
||||
// Write parameters to stdin as a single JSON line.
|
||||
// Secrets are merged into the parameters map by the caller, so the
|
||||
// action reads everything with a single readline().
|
||||
if let Some(params_data) = parameters_stdin {
|
||||
if has_real_params {
|
||||
if let Err(e) = stdin.write_all(params_data.as_bytes()).await {
|
||||
error = Some(format!("Failed to write parameters to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n---ATTUNE_PARAMS_END---\n").await {
|
||||
error = Some(format!("Failed to write parameter delimiter: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write secrets as JSON (always, for backward compatibility)
|
||||
if error.is_none() && !secrets.is_empty() {
|
||||
match serde_json::to_string(secrets) {
|
||||
Ok(secrets_json) => {
|
||||
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
|
||||
error = Some(format!("Failed to write secrets to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
error = Some(format!("Failed to write newline to stdin: {}", e));
|
||||
}
|
||||
}
|
||||
Err(e) => error = Some(format!("Failed to serialize secrets: {}", e)),
|
||||
if let Err(e) = stdin.write_all(params_data.as_bytes()).await {
|
||||
error = Some(format!("Failed to write parameters to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
error = Some(format!("Failed to write newline to stdin: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +318,12 @@ impl ShellRuntime {
|
||||
script.push_str("declare -A ATTUNE_SECRETS\n");
|
||||
for (key, value) in &context.secrets {
|
||||
let escaped_key = bash_single_quote_escape(key);
|
||||
let escaped_val = bash_single_quote_escape(value);
|
||||
// Serialize structured JSON values to string for bash; plain strings used directly.
|
||||
let val_str = match value {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
let escaped_val = bash_single_quote_escape(&val_str);
|
||||
script.push_str(&format!(
|
||||
"ATTUNE_SECRETS['{}']='{}'\n",
|
||||
escaped_key, escaped_val
|
||||
@@ -388,7 +373,7 @@ impl ShellRuntime {
|
||||
async fn execute_shell_file(
|
||||
&self,
|
||||
script_path: PathBuf,
|
||||
secrets: &std::collections::HashMap<String, String>,
|
||||
_secrets: &std::collections::HashMap<String, String>,
|
||||
env: &std::collections::HashMap<String, String>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout_secs: Option<u64>,
|
||||
@@ -396,11 +381,7 @@ impl ShellRuntime {
|
||||
max_stderr_bytes: usize,
|
||||
output_format: OutputFormat,
|
||||
) -> RuntimeResult<ExecutionResult> {
|
||||
debug!(
|
||||
"Executing shell file: {:?} with {} secrets",
|
||||
script_path,
|
||||
secrets.len()
|
||||
);
|
||||
debug!("Executing shell file: {:?}", script_path,);
|
||||
|
||||
// Build command
|
||||
let mut cmd = Command::new(&self.shell_path);
|
||||
@@ -413,7 +394,7 @@ impl ShellRuntime {
|
||||
|
||||
self.execute_with_streaming(
|
||||
cmd,
|
||||
secrets,
|
||||
&std::collections::HashMap::new(),
|
||||
parameters_stdin,
|
||||
timeout_secs,
|
||||
max_stdout_bytes,
|
||||
@@ -463,6 +444,13 @@ impl Runtime for ShellRuntime {
|
||||
context.parameters
|
||||
);
|
||||
|
||||
// Merge secrets into parameters as a single JSON document.
|
||||
// Actions receive everything via one readline() on stdin.
|
||||
let mut merged_parameters = context.parameters.clone();
|
||||
for (key, value) in &context.secrets {
|
||||
merged_parameters.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
// Prepare environment and parameters according to delivery method
|
||||
let mut env = context.env.clone();
|
||||
let config = ParameterDeliveryConfig {
|
||||
@@ -471,7 +459,7 @@ impl Runtime for ShellRuntime {
|
||||
};
|
||||
|
||||
let prepared_params =
|
||||
parameter_passing::prepare_parameters(&context.parameters, &mut env, config)?;
|
||||
parameter_passing::prepare_parameters(&merged_parameters, &mut env, config)?;
|
||||
|
||||
// Get stdin content if parameters are delivered via stdin
|
||||
let parameters_stdin = prepared_params.stdin_content();
|
||||
@@ -486,12 +474,13 @@ impl Runtime for ShellRuntime {
|
||||
info!("No parameters will be sent via stdin");
|
||||
}
|
||||
|
||||
// If code_path is provided, execute the file directly
|
||||
// If code_path is provided, execute the file directly.
|
||||
// Secrets are already merged into parameters — no separate secrets arg needed.
|
||||
if let Some(code_path) = &context.code_path {
|
||||
return self
|
||||
.execute_shell_file(
|
||||
code_path.clone(),
|
||||
&context.secrets,
|
||||
&HashMap::new(),
|
||||
&env,
|
||||
parameters_stdin,
|
||||
context.timeout,
|
||||
@@ -747,8 +736,11 @@ mod tests {
|
||||
env: HashMap::new(),
|
||||
secrets: {
|
||||
let mut s = HashMap::new();
|
||||
s.insert("api_key".to_string(), "secret_key_12345".to_string());
|
||||
s.insert("db_password".to_string(), "super_secret_pass".to_string());
|
||||
s.insert("api_key".to_string(), serde_json::json!("secret_key_12345"));
|
||||
s.insert(
|
||||
"db_password".to_string(),
|
||||
serde_json::json!("super_secret_pass"),
|
||||
);
|
||||
s
|
||||
},
|
||||
timeout: Some(10),
|
||||
|
||||
Reference in New Issue
Block a user