Skip to main content

hm_plugin_protocol/
ir.rs

1//! Pipeline IR, the v0 wire format consumed by the `hm` binary.
2//!
3//! Source of truth lives in two other places that must stay in sync
4//! with this file: `harmont-pipeline/src/Harmont/Pipeline/Schema.hs`
5//! (Haskell mirror) and `cidsl/py/harmont/__init__.py` (Python emitter).
6//! Changing a field name here means changing it in both other places
7//! in the same PR.
8
9use std::collections::BTreeMap;
10
11use schemars::JsonSchema as DeriveJsonSchema;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
15pub struct Pipeline {
16    /// Must equal `"0"` — bumping this is reserved for breaking
17    /// schema changes, none of which are scheduled. The v0 schema
18    /// gains optional fields in-place (see `runner` below).
19    pub version: String,
20    #[serde(default)]
21    pub env: Option<BTreeMap<String, String>>,
22    #[serde(default)]
23    pub default_image: Option<String>,
24    pub steps: Vec<Step>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
28#[serde(tag = "type", rename_all = "snake_case")]
29pub enum Step {
30    Command(Box<CommandStep>),
31    Wait(WaitStep),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
35pub struct CommandStep {
36    pub key: String,
37    #[serde(default)]
38    pub label: Option<String>,
39    pub cmd: String,
40    #[serde(default)]
41    pub builds_in: Option<String>,
42    #[serde(default)]
43    pub image: Option<String>,
44    #[serde(default)]
45    pub env: Option<BTreeMap<String, String>>,
46    #[serde(default)]
47    pub timeout_seconds: Option<u32>,
48    #[serde(default)]
49    pub cache: Option<Cache>,
50
51    /// Names the step-executor plugin that should run this step.
52    /// `None` ⇒ the default executor handles it (Docker, in the
53    /// shipped configuration).
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub runner: Option<String>,
56
57    /// Plugin-specific extra fields. Validated by the executor
58    /// plugin's `StepExecutorSpec::step_schema` if it set one.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub runner_args: Option<serde_json::Value>,
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
64pub struct WaitStep {
65    #[serde(default)]
66    pub continue_on_failure: bool,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
70pub struct Cache {
71    pub policy: String,
72    #[serde(default)]
73    pub key: Option<String>,
74}
75
76#[cfg(test)]
77#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn parses_step_with_runner() {
83        let json = br#"{
84            "version": "0",
85            "steps": [
86                {"type": "command", "key": "a", "cmd": "echo a"},
87                {"type": "command", "key": "b", "cmd": "freestyle run",
88                 "runner": "freestyle", "runner_args": {"region": "us"}}
89            ]
90        }"#;
91        let p: Pipeline = serde_json::from_slice(json).unwrap();
92        let Step::Command(b) = &p.steps[1] else {
93            panic!("expected command")
94        };
95        assert_eq!(b.runner.as_deref(), Some("freestyle"));
96        assert_eq!(b.runner_args.as_ref().unwrap()["region"], "us");
97    }
98
99    #[test]
100    fn parses_legacy_step_without_runner() {
101        let json = br#"{
102            "version": "0",
103            "steps": [{"type": "command", "key": "a", "cmd": "echo a"}]
104        }"#;
105        let p: Pipeline = serde_json::from_slice(json).unwrap();
106        let Step::Command(a) = &p.steps[0] else {
107            panic!("expected command")
108        };
109        assert!(a.runner.is_none());
110        assert!(a.runner_args.is_none());
111    }
112}