Skip to main content

anodizer_core/config/
hooks.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize};
3
4// ---------------------------------------------------------------------------
5// HooksConfig
6// ---------------------------------------------------------------------------
7
8/// Top-level lifecycle hooks for `before` and `after` blocks.
9/// Each block has `pre` and `post` lists of hook commands that run around the
10/// entire pipeline (not individual stages).
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
12#[serde(default)]
13pub struct HooksConfig {
14    /// Commands to run before the pipeline or stage starts. Matches GoReleaser
15    /// `before.hooks` canonically.
16    pub hooks: Option<Vec<HookEntry>>,
17    /// Commands to run after the pipeline or stage completes. Anodizer extension
18    /// (GoReleaser has no top-level `after:` block).
19    pub post: Option<Vec<HookEntry>>,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
23#[serde(default)]
24pub struct StructuredHook {
25    /// Command to run.
26    ///
27    /// The entire string is interpreted by `sh -c`, so shell metacharacters
28    /// (`|`, `;`, `&&`, backticks, `$()`, redirects, globs) are honoured —
29    /// any templated values folded into `cmd` become part of the shell
30    /// command and are subject to word-splitting and metacharacter expansion.
31    /// Keep templated user-config values out of `cmd` when possible, or quote
32    /// them defensively (e.g. `'{{ .Env.FOO }}'`). Hooks already run with
33    /// `env_clear()` plus an allow-list, so secrets in `$ENV` are not
34    /// inherited unless explicitly listed in `env`.
35    pub cmd: String,
36    /// Working directory for the command (defaults to project root).
37    pub dir: Option<String>,
38    /// Environment variables for the command.
39    #[serde(default)]
40    pub env: Option<Vec<String>>,
41    /// When true, capture and log stdout/stderr of the command.
42    pub output: Option<bool>,
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
46#[serde(untagged)]
47pub enum HookEntry {
48    Simple(String),
49    Structured(StructuredHook),
50}
51
52impl PartialEq<&str> for HookEntry {
53    fn eq(&self, other: &&str) -> bool {
54        match self {
55            HookEntry::Simple(s) => s.as_str() == *other,
56            HookEntry::Structured(h) => h.cmd.as_str() == *other,
57        }
58    }
59}
60
61impl<'de> Deserialize<'de> for HookEntry {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: Deserializer<'de>,
65    {
66        let value = serde_json::Value::deserialize(deserializer)?;
67        match &value {
68            serde_json::Value::String(s) => Ok(HookEntry::Simple(s.clone())),
69            serde_json::Value::Object(_) => {
70                let hook: StructuredHook =
71                    serde_json::from_value(value).map_err(serde::de::Error::custom)?;
72                Ok(HookEntry::Structured(hook))
73            }
74            _ => Err(serde::de::Error::custom(
75                "hook entry must be a string or an object with cmd/dir/env/output",
76            )),
77        }
78    }
79}