Skip to main content

sr_core/
hooks.rs

1//! Shell execution helper.
2//!
3//! The only thing in sr that runs user-visible shell commands is the
4//! `Publish` stage — it shells out to `cargo publish` / `npm publish` /
5//! `docker buildx build` / `uv publish` / `twine` when a typed publisher
6//! needs to hit a registry, or to a `publish: custom` command.
7//!
8//! sr intentionally does not run user "hooks" (pre_release, post_release,
9//! build). Those belong in the CI workflow around `sr plan` / `sr prepare` /
10//! `sr release`, not inside sr.
11
12use crate::error::ReleaseError;
13
14/// Run a shell command (`sh -c`). Inherits stdio unless `stdin_data` is
15/// provided (in which case stdin is piped). `env` is injected into the
16/// child process; `RELSTATE_*` / `SR_VERSION` etc. flow through here.
17pub fn run_shell(
18    cmd: &str,
19    stdin_data: Option<&str>,
20    env: &[(&str, &str)],
21) -> Result<(), ReleaseError> {
22    let mut child = {
23        let mut builder = std::process::Command::new("sh");
24        builder.args(["-c", cmd]);
25        for &(k, v) in env {
26            builder.env(k, v);
27        }
28        if stdin_data.is_some() {
29            builder.stdin(std::process::Stdio::piped());
30        } else {
31            builder.stdin(std::process::Stdio::inherit());
32        }
33        builder
34            .spawn()
35            .map_err(|e| ReleaseError::Hook(format!("{cmd}: {e}")))?
36    };
37
38    if let Some(data) = stdin_data
39        && let Some(ref mut stdin) = child.stdin
40    {
41        use std::io::Write;
42        let _ = stdin.write_all(data.as_bytes());
43    }
44
45    let status = child
46        .wait()
47        .map_err(|e| ReleaseError::Hook(format!("{cmd}: {e}")))?;
48
49    if !status.success() {
50        let code = status.code().unwrap_or(1);
51        return Err(ReleaseError::Hook(format!("{cmd} exited with code {code}")));
52    }
53
54    Ok(())
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn run_shell_success() {
63        run_shell("true", None, &[]).unwrap();
64    }
65
66    #[test]
67    fn run_shell_failure() {
68        let result = run_shell("false", None, &[]);
69        assert!(result.is_err());
70    }
71
72    #[test]
73    fn run_shell_with_env() {
74        run_shell("test \"$MY_VAR\" = hello", None, &[("MY_VAR", "hello")]).unwrap();
75    }
76}