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}