use crate::config::{self, HookEntry};
use crate::log::StageLogger;
use crate::template::{self, TemplateVars};
use anyhow::{Context as _, Result};
use std::process::Command;
fn redact_secrets(output: &str) -> String {
let env: Vec<(String, String)> = std::env::vars().collect();
crate::redact::string(output, &env)
}
fn render_hook_template(template: &str, vars: &TemplateVars, label: &str) -> Result<String> {
template::render(template, vars)
.with_context(|| format!("{} hook: render template '{}'", label, template))
}
pub fn run_hooks(
hooks: &[HookEntry],
label: &str,
dry_run: bool,
log: &StageLogger,
template_vars: Option<&TemplateVars>,
) -> Result<()> {
for hook in hooks {
let (raw_cmd, raw_dir, env, output_flag) = match hook {
HookEntry::Simple(s) => (s.as_str(), None, None, None),
HookEntry::Structured(h) => {
(h.cmd.as_str(), h.dir.as_deref(), h.env.as_ref(), h.output)
}
};
let cmd_str = if let Some(tv) = template_vars {
render_hook_template(raw_cmd, tv, label)?
} else {
raw_cmd.to_string()
};
let dir_str = match raw_dir {
Some(d) => Some(if let Some(tv) = template_vars {
render_hook_template(d, tv, label)?
} else {
d.to_string()
}),
None => None,
};
let expanded_env: Option<Vec<(String, String)>> = match env {
Some(envs) => {
let pairs = if let Some(tv) = template_vars {
config::render_env_entries(envs, |s| render_hook_template(s, tv, label))
.with_context(|| format!("{label} hook: render env entries"))?
} else {
config::parse_env_entries(envs)
.with_context(|| format!("{label} hook: parse env entries"))?
};
Some(pairs)
}
None => None,
};
if dry_run {
log.status(&format!("[dry-run] {} hook: {}", label, cmd_str));
} else {
log.status(&format!("running {} hook: {}", label, cmd_str));
let mut command = Command::new("sh");
command.arg("-c").arg(&cmd_str);
if let Some(ref d) = dir_str {
command.current_dir(d);
}
if let Some(ref envs) = expanded_env {
for (k, v) in envs {
command.env(k, v);
}
}
let output = command
.output()
.with_context(|| format!("failed to spawn {} hook: {}", label, cmd_str))?;
let redacted_stdout = redact_secrets(&String::from_utf8_lossy(&output.stdout));
let redacted_stderr = redact_secrets(&String::from_utf8_lossy(&output.stderr));
if output_flag == Some(true) && !redacted_stdout.trim().is_empty() {
log.status(&format!("[hook output] {}", redacted_stdout.trim()));
}
let redacted_output = std::process::Output {
status: output.status,
stdout: redacted_stdout.into_bytes(),
stderr: redacted_stderr.into_bytes(),
};
log.check_output(redacted_output, &format!("{} hook: {}", label, cmd_str))?;
}
}
Ok(())
}