mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
Add [sandbox.env] support to workflow configs
Allow passing environment variables into sandbox command execution via
`[sandbox.env]` in TOML configs. Supports literal values and host env
passthrough via `${env.VARNAME}` syntax (whole-value only, missing vars
are hard errors). Env vars are injected into command nodes via
`cmd.envs()` and into CLI backend agents via the sandbox env file.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
36d35d07fe
commit
5d6691df22
17 changed files with 324 additions and 10 deletions
|
|
@ -932,6 +932,7 @@ mod runs {
|
|||
network: Some(arc_workflows::daytona_sandbox::DaytonaNetwork::Block),
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
vars: Some(std::collections::HashMap::from([
|
||||
("repo_url".into(), "https://github.com/org/api-server".into()),
|
||||
|
|
@ -1052,6 +1053,7 @@ mod workflows {
|
|||
network: None,
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
vars: Some(std::collections::HashMap::from([
|
||||
("repo_url".into(), "https://github.com/org/service".into()),
|
||||
|
|
@ -1117,6 +1119,7 @@ mod workflows {
|
|||
network: None,
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
vars: Some(std::collections::HashMap::from([
|
||||
("spec_path".into(), "specs/feature.md".into()),
|
||||
|
|
@ -1193,6 +1196,7 @@ mod workflows {
|
|||
network: None,
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
vars: Some(std::collections::HashMap::from([
|
||||
("source_env".into(), "production".into()),
|
||||
|
|
@ -1260,6 +1264,7 @@ mod workflows {
|
|||
network: None,
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
vars: Some(std::collections::HashMap::from([
|
||||
("analytics_window".into(), "30d".into()),
|
||||
|
|
@ -2653,6 +2658,7 @@ mod settings {
|
|||
network: Some(arc_workflows::daytona_sandbox::DaytonaNetwork::Block),
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
vars: None,
|
||||
checkpoint: Default::default(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -330,16 +331,32 @@ pub fn parse_cli_response(provider: Provider, output: &str) -> Option<CliRespons
|
|||
}
|
||||
}
|
||||
|
||||
/// Escape a value for safe embedding inside single quotes in a shell command.
|
||||
fn shell_escape(val: &str) -> String {
|
||||
val.replace('\'', "'\\''")
|
||||
}
|
||||
|
||||
/// CLI backend that invokes external CLI tools (claude, codex, gemini) via `exec_command()`.
|
||||
pub struct AgentCliBackend {
|
||||
model: String,
|
||||
provider: Provider,
|
||||
env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl AgentCliBackend {
|
||||
#[must_use]
|
||||
pub fn new(model: String, provider: Provider) -> Self {
|
||||
Self { model, provider }
|
||||
Self {
|
||||
model,
|
||||
provider,
|
||||
env: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
|
||||
self.env = env;
|
||||
self
|
||||
}
|
||||
|
||||
/// Detect changed files by comparing git state before and after the CLI run.
|
||||
|
|
@ -455,8 +472,11 @@ impl CodergenBackend for AgentCliBackend {
|
|||
env_lines.extend(provider.api_key_env_vars().iter().filter_map(|name| {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.map(|val| format!("export {name}='{val}'"))
|
||||
.map(|val| format!("export {name}='{}'", shell_escape(&val)))
|
||||
}));
|
||||
for (name, val) in &self.env {
|
||||
env_lines.push(format!("export {name}='{}'", shell_escape(val)));
|
||||
}
|
||||
{
|
||||
sandbox
|
||||
.write_file(&env_path, &env_lines.join("\n"))
|
||||
|
|
|
|||
|
|
@ -659,13 +659,23 @@ pub async fn run_command(
|
|||
);
|
||||
|
||||
// 7. Build engine
|
||||
let registry = default_registry(interviewer.clone(), || {
|
||||
if dry_run_mode {
|
||||
None
|
||||
} else {
|
||||
let api = AgentApiBackend::new(model.clone(), provider_enum, fallback_chain.clone());
|
||||
let cli = AgentCliBackend::new(model.clone(), provider_enum);
|
||||
Some(Box::new(BackendRouter::new(Box::new(api), cli)))
|
||||
let sandbox_env: HashMap<String, String> = run_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.and_then(|s| s.env.clone())
|
||||
.unwrap_or_default();
|
||||
let registry = default_registry(interviewer.clone(), {
|
||||
let sandbox_env = sandbox_env.clone();
|
||||
let model = model.clone();
|
||||
move || {
|
||||
if dry_run_mode {
|
||||
None
|
||||
} else {
|
||||
let api = AgentApiBackend::new(model.clone(), provider_enum, fallback_chain.clone());
|
||||
let cli = AgentCliBackend::new(model.clone(), provider_enum)
|
||||
.with_env(sandbox_env.clone());
|
||||
Some(Box::new(BackendRouter::new(Box::new(api), cli)))
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut engine = WorkflowRunEngine::with_interviewer(
|
||||
|
|
@ -674,6 +684,9 @@ pub async fn run_command(
|
|||
interviewer,
|
||||
Arc::clone(&sandbox),
|
||||
);
|
||||
if !sandbox_env.is_empty() {
|
||||
engine.set_env(sandbox_env);
|
||||
}
|
||||
|
||||
// Wire up hook runner from run config
|
||||
if let Some(ref cfg) = run_cfg {
|
||||
|
|
@ -1740,6 +1753,7 @@ mod tests {
|
|||
preserve: Some(false),
|
||||
daytona: None,
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
vars: None,
|
||||
hooks: Vec::new(),
|
||||
|
|
@ -1763,6 +1777,7 @@ mod tests {
|
|||
preserve: Some(true),
|
||||
daytona: None,
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
vars: None,
|
||||
hooks: Vec::new(),
|
||||
|
|
@ -1774,6 +1789,7 @@ mod tests {
|
|||
preserve: Some(false),
|
||||
daytona: None,
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -1788,6 +1804,7 @@ mod tests {
|
|||
preserve: Some(true),
|
||||
daytona: None,
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ pub struct SandboxConfig {
|
|||
pub preserve: Option<bool>,
|
||||
pub daytona: Option<DaytonaConfig>,
|
||||
pub exe: Option<arc_exe::ExeConfig>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// Defaults for workflow runs, loaded from the server config.
|
||||
|
|
@ -135,6 +136,13 @@ impl WorkflowRunConfig {
|
|||
(None, Some(_)) => task.daytona = default.daytona.clone(),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ref default_env) = default.env {
|
||||
let mut merged = default_env.clone();
|
||||
if let Some(ref task_env) = task.env {
|
||||
merged.extend(task_env.clone());
|
||||
}
|
||||
task.env = Some(merged);
|
||||
}
|
||||
}
|
||||
(None, Some(_)) => self.sandbox = defaults.sandbox.clone(),
|
||||
_ => {}
|
||||
|
|
@ -171,10 +179,47 @@ pub fn load_run_config(path: &Path) -> anyhow::Result<WorkflowRunConfig> {
|
|||
|
||||
let config_dir = path.parent().unwrap_or(Path::new("."));
|
||||
resolve_dockerfile(&mut config, config_dir)?;
|
||||
resolve_sandbox_env(&mut config)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Resolve `${env.VARNAME}` references in `[sandbox.env]` values.
|
||||
///
|
||||
/// Only whole-value references are supported (no partial interpolation).
|
||||
/// Missing host env vars produce a hard error.
|
||||
fn resolve_sandbox_env(config: &mut WorkflowRunConfig) -> anyhow::Result<()> {
|
||||
if let Some(env) = config
|
||||
.sandbox
|
||||
.as_mut()
|
||||
.and_then(|s| s.env.as_mut())
|
||||
{
|
||||
resolve_env_refs(env)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve `${env.VARNAME}` patterns in a map of env vars.
|
||||
///
|
||||
/// If the entire value is `${env.VARNAME}`, it is replaced with the host
|
||||
/// environment variable. Any other value is left as-is. Missing host
|
||||
/// variables produce an error.
|
||||
pub fn resolve_env_refs(env: &mut HashMap<String, String>) -> anyhow::Result<()> {
|
||||
for (key, value) in env.iter_mut() {
|
||||
if let Some(var_name) = value
|
||||
.strip_prefix("${env.")
|
||||
.and_then(|s| s.strip_suffix('}'))
|
||||
{
|
||||
*value = std::env::var(var_name).with_context(|| {
|
||||
format!(
|
||||
"sandbox.env.{key}: host environment variable {var_name:?} is not set"
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If the config contains a `dockerfile = { path = "..." }`, read the file
|
||||
/// and replace it with `DockerfileSource::Inline(contents)`.
|
||||
fn resolve_dockerfile(config: &mut WorkflowRunConfig, config_dir: &Path) -> anyhow::Result<()> {
|
||||
|
|
@ -882,6 +927,7 @@ preserve = true
|
|||
preserve: Some(false),
|
||||
daytona: None,
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -908,6 +954,7 @@ provider = "docker"
|
|||
preserve: Some(true),
|
||||
daytona: None,
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -937,6 +984,7 @@ provider = "daytona"
|
|||
..DaytonaConfig::default()
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -973,6 +1021,7 @@ auto_stop_interval = 60
|
|||
..DaytonaConfig::default()
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -1008,6 +1057,7 @@ env = "from_task"
|
|||
..DaytonaConfig::default()
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -1047,6 +1097,7 @@ cpu = 2
|
|||
..DaytonaConfig::default()
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -1085,6 +1136,7 @@ auto_stop_interval = 60
|
|||
..DaytonaConfig::default()
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -1318,6 +1370,7 @@ network = "block"
|
|||
..DaytonaConfig::default()
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -1352,6 +1405,7 @@ auto_stop_interval = 60
|
|||
..DaytonaConfig::default()
|
||||
}),
|
||||
exe: None,
|
||||
env: None,
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
|
|
@ -1477,4 +1531,189 @@ exclude_globs = ["**/node_modules/**"]
|
|||
vec!["**/node_modules/**"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_toml_with_sandbox_env() {
|
||||
let toml = r#"
|
||||
version = 1
|
||||
goal = "test"
|
||||
graph = "w.dot"
|
||||
|
||||
[sandbox.env]
|
||||
FOO = "bar"
|
||||
BAZ = "${env.HOME}"
|
||||
"#;
|
||||
let config = parse_run_config(toml).unwrap();
|
||||
let env = config.sandbox.unwrap().env.unwrap();
|
||||
assert_eq!(env["FOO"], "bar");
|
||||
// Not yet resolved (parse_run_config doesn't resolve env refs)
|
||||
assert_eq!(env["BAZ"], "${env.HOME}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_toml_without_sandbox_env() {
|
||||
let toml = r#"
|
||||
version = 1
|
||||
goal = "test"
|
||||
graph = "w.dot"
|
||||
|
||||
[sandbox]
|
||||
provider = "daytona"
|
||||
"#;
|
||||
let config = parse_run_config(toml).unwrap();
|
||||
assert!(config.sandbox.unwrap().env.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_env_refs_literal_passthrough() {
|
||||
let mut env = HashMap::from([("FOO".into(), "bar".into())]);
|
||||
resolve_env_refs(&mut env).unwrap();
|
||||
assert_eq!(env["FOO"], "bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_env_refs_host_var() {
|
||||
std::env::set_var("ARC_TEST_RESOLVE_VAR", "secret123");
|
||||
let mut env = HashMap::from([
|
||||
("MY_KEY".into(), "${env.ARC_TEST_RESOLVE_VAR}".into()),
|
||||
]);
|
||||
resolve_env_refs(&mut env).unwrap();
|
||||
assert_eq!(env["MY_KEY"], "secret123");
|
||||
std::env::remove_var("ARC_TEST_RESOLVE_VAR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_env_refs_missing_var_errors() {
|
||||
let mut env = HashMap::from([
|
||||
("MY_KEY".into(), "${env.ARC_TEST_NONEXISTENT_VAR_12345}".into()),
|
||||
]);
|
||||
let err = resolve_env_refs(&mut env).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("ARC_TEST_NONEXISTENT_VAR_12345"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_env_refs_partial_not_interpolated() {
|
||||
let mut env = HashMap::from([
|
||||
("MIXED".into(), "prefix_${env.HOME}_suffix".into()),
|
||||
]);
|
||||
// Partial interpolation is not supported — value is left as-is
|
||||
resolve_env_refs(&mut env).unwrap();
|
||||
assert_eq!(env["MIXED"], "prefix_${env.HOME}_suffix");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_defaults_merges_sandbox_env() {
|
||||
let mut cfg = parse_run_config(
|
||||
r#"
|
||||
version = 1
|
||||
goal = "test"
|
||||
graph = "w.dot"
|
||||
|
||||
[sandbox.env]
|
||||
TASK_KEY = "task_val"
|
||||
SHARED = "from_task"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let defaults = RunDefaults {
|
||||
sandbox: Some(SandboxConfig {
|
||||
provider: None,
|
||||
preserve: None,
|
||||
daytona: None,
|
||||
exe: None,
|
||||
env: Some(HashMap::from([
|
||||
("DEFAULT_KEY".into(), "default_val".into()),
|
||||
("SHARED".into(), "from_default".into()),
|
||||
])),
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
cfg.apply_defaults(&defaults);
|
||||
let env = cfg.sandbox.unwrap().env.unwrap();
|
||||
assert_eq!(env["DEFAULT_KEY"], "default_val");
|
||||
assert_eq!(env["TASK_KEY"], "task_val");
|
||||
assert_eq!(env["SHARED"], "from_task");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_defaults_sandbox_env_from_default_only() {
|
||||
let mut cfg = parse_run_config(
|
||||
r#"
|
||||
version = 1
|
||||
goal = "test"
|
||||
graph = "w.dot"
|
||||
|
||||
[sandbox]
|
||||
provider = "daytona"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let defaults = RunDefaults {
|
||||
sandbox: Some(SandboxConfig {
|
||||
provider: None,
|
||||
preserve: None,
|
||||
daytona: None,
|
||||
exe: None,
|
||||
env: Some(HashMap::from([("KEY".into(), "val".into())])),
|
||||
}),
|
||||
..RunDefaults::default()
|
||||
};
|
||||
cfg.apply_defaults(&defaults);
|
||||
let env = cfg.sandbox.unwrap().env.unwrap();
|
||||
assert_eq!(env["KEY"], "val");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_run_config_resolves_env_refs() {
|
||||
std::env::set_var("ARC_TEST_LOAD_ENV", "resolved_value");
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let toml_path = dir.path().join("run.toml");
|
||||
std::fs::write(
|
||||
&toml_path,
|
||||
r#"
|
||||
version = 1
|
||||
goal = "test"
|
||||
graph = "w.dot"
|
||||
|
||||
[sandbox.env]
|
||||
LITERAL = "hello"
|
||||
FROM_HOST = "${env.ARC_TEST_LOAD_ENV}"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_run_config(&toml_path).unwrap();
|
||||
let env = config.sandbox.unwrap().env.unwrap();
|
||||
assert_eq!(env["LITERAL"], "hello");
|
||||
assert_eq!(env["FROM_HOST"], "resolved_value");
|
||||
std::env::remove_var("ARC_TEST_LOAD_ENV");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_run_config_missing_env_var_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let toml_path = dir.path().join("run.toml");
|
||||
std::fs::write(
|
||||
&toml_path,
|
||||
r#"
|
||||
version = 1
|
||||
goal = "test"
|
||||
graph = "w.dot"
|
||||
|
||||
[sandbox.env]
|
||||
MISSING = "${env.ARC_TEST_DEFINITELY_NOT_SET_67890}"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err = load_run_config(&toml_path).unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("ARC_TEST_DEFINITELY_NOT_SET_67890"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -860,6 +860,7 @@ impl WorkflowRunEngine {
|
|||
sandbox,
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
},
|
||||
interviewer: None,
|
||||
}
|
||||
|
|
@ -875,6 +876,7 @@ impl WorkflowRunEngine {
|
|||
sandbox: Arc::clone(&services.sandbox),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: services.hook_runner.clone(),
|
||||
env: services.env.clone(),
|
||||
},
|
||||
interviewer: None,
|
||||
}
|
||||
|
|
@ -895,6 +897,7 @@ impl WorkflowRunEngine {
|
|||
sandbox,
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
},
|
||||
interviewer: Some(interviewer),
|
||||
}
|
||||
|
|
@ -905,6 +908,11 @@ impl WorkflowRunEngine {
|
|||
self.services.hook_runner = Some(runner);
|
||||
}
|
||||
|
||||
/// Set environment variables from `[sandbox.env]` config.
|
||||
pub fn set_env(&mut self, env: HashMap<String, String>) {
|
||||
self.services.env = env;
|
||||
}
|
||||
|
||||
/// Run lifecycle hooks and return the merged decision.
|
||||
/// Returns `Proceed` if no hook runner is configured.
|
||||
async fn run_hooks(
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -458,6 +459,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
};
|
||||
|
||||
let outcome = handler
|
||||
|
|
@ -519,6 +521,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
};
|
||||
|
||||
let outcome = handler
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ impl Handler for CommandHandler {
|
|||
context: &Context,
|
||||
_graph: &Graph,
|
||||
logs_root: &Path,
|
||||
_services: &EngineServices,
|
||||
services: &EngineServices,
|
||||
) -> Result<Outcome, ArcError> {
|
||||
let script = node
|
||||
.attrs
|
||||
|
|
@ -69,11 +69,13 @@ impl Handler for CommandHandler {
|
|||
tokio::process::Command::new("python3")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.envs(&services.env)
|
||||
.output()
|
||||
} else {
|
||||
tokio::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.envs(&services.env)
|
||||
.output()
|
||||
};
|
||||
|
||||
|
|
@ -183,6 +185,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -281,6 +281,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -396,6 +397,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
};
|
||||
|
||||
let handler = SubWorkflowHandler;
|
||||
|
|
@ -504,6 +506,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
};
|
||||
|
||||
let handler = SubWorkflowHandler;
|
||||
|
|
@ -564,6 +567,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
};
|
||||
|
||||
let handler = SubWorkflowHandler;
|
||||
|
|
@ -736,6 +740,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
};
|
||||
|
||||
let handler = SubWorkflowHandler;
|
||||
|
|
@ -820,6 +825,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
};
|
||||
|
||||
let handler = SubWorkflowHandler;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ pub struct EngineServices {
|
|||
pub(crate) git_state: std::sync::RwLock<Option<Arc<GitState>>>,
|
||||
/// Hook runner for user-defined lifecycle hooks.
|
||||
pub hook_runner: Option<Arc<HookRunner>>,
|
||||
/// Environment variables from `[sandbox.env]` config, injected into command nodes.
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl EngineServices {
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ impl Handler for ParallelHandler {
|
|||
let registry = Arc::clone(&services.registry);
|
||||
let emitter = Arc::clone(&services.emitter);
|
||||
let hook_runner = services.hook_runner.clone();
|
||||
let env = services.env.clone();
|
||||
let graph = graph.clone();
|
||||
let logs_root = logs_root.to_path_buf();
|
||||
let sem = Arc::clone(&semaphore);
|
||||
|
|
@ -438,6 +439,7 @@ impl Handler for ParallelHandler {
|
|||
sandbox: Arc::clone(&setup.sandbox),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: hook_runner.clone(),
|
||||
env: env.clone(),
|
||||
};
|
||||
let handler = registry.resolve(target_node);
|
||||
let outcome = handler
|
||||
|
|
@ -771,6 +773,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ mod tests {
|
|||
)),
|
||||
git_state: std::sync::RwLock::new(None),
|
||||
hook_runner: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue