Skip to main content

sr_core/publishers/
custom.rs

1//! Custom publisher: user-supplied shell command + optional state check.
2//!
3//! - check: run `check` command (if provided). Exit 0 → Completed, nonzero
4//!   → Needed. When absent, returns Unknown so `run` always executes.
5//! - run: run `command` in `cwd` (defaults to package path).
6
7use super::{PublishCtx, PublishState, Publisher};
8use crate::error::ReleaseError;
9use crate::hooks::run_shell;
10
11pub struct CustomPublisher {
12    pub command: String,
13    pub check: Option<String>,
14    pub cwd: Option<String>,
15}
16
17impl Publisher for CustomPublisher {
18    fn name(&self) -> &'static str {
19        "custom"
20    }
21
22    fn check(&self, ctx: &PublishCtx<'_>) -> Result<PublishState, ReleaseError> {
23        let Some(check_cmd) = &self.check else {
24            return Ok(PublishState::Unknown(
25                "no `check` command configured".into(),
26            ));
27        };
28
29        let cwd = self.cwd.clone().unwrap_or_else(|| ctx.package.path.clone());
30        let wrapped = format!("cd {} && {}", shell_word(&cwd), check_cmd);
31
32        // Run without propagating stderr to user unless it fails; exit 0 =
33        // already published, anything else = needed. Transient failures
34        // (e.g. network) would be reported as "needed" here — acceptable,
35        // since the publish command itself will fail cleanly.
36        match run_shell(&wrapped, None, ctx.env) {
37            Ok(()) => Ok(PublishState::Completed),
38            Err(_) => Ok(PublishState::Needed),
39        }
40    }
41
42    fn run(&self, ctx: &PublishCtx<'_>) -> Result<(), ReleaseError> {
43        let cwd = self.cwd.clone().unwrap_or_else(|| ctx.package.path.clone());
44        let wrapped = format!("cd {} && {}", shell_word(&cwd), self.command);
45
46        if ctx.dry_run {
47            eprintln!("[dry-run] custom ({cwd}): {}", self.command);
48            return Ok(());
49        }
50
51        eprintln!("custom ({cwd}): {}", self.command);
52        run_shell(&wrapped, None, ctx.env)
53    }
54}
55
56fn shell_word(s: &str) -> String {
57    let mut out = String::from("'");
58    for ch in s.chars() {
59        if ch == '\'' {
60            out.push_str("'\\''");
61        } else {
62            out.push(ch);
63        }
64    }
65    out.push('\'');
66    out
67}