Skip to main content

wfe_core/models/
workflow_definition.rs

1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5use super::condition::StepCondition;
6use super::error_behavior::ErrorBehavior;
7use super::service::ServiceDefinition;
8
9/// Declaration of a volume that persists across every step in a workflow
10/// run, including sub-workflows started via `type: workflow` steps. Backends
11/// that support it (currently just Kubernetes) provision a single volume
12/// per top-level workflow instance and mount it on every step container at
13/// `mount_path`. Sub-workflows see the same volume because they share the
14/// parent's isolation domain (namespace, in the K8s case).
15///
16/// Declared once on the top-level workflow (e.g. `ci`) that orchestrates
17/// the sub-workflows. Declarations on non-root workflows are ignored in
18/// favor of the root's declaration.
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20pub struct SharedVolume {
21    /// Absolute path the volume is mounted at inside every step container.
22    /// Typical value: `/workspace`.
23    pub mount_path: String,
24    /// Optional size override (e.g. `"20Gi"`). When unset the backend falls
25    /// back to its configured default (ClusterConfig::default_shared_volume_size
26    /// for the Kubernetes executor).
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub size: Option<String>,
29}
30
31impl Default for SharedVolume {
32    fn default() -> Self {
33        Self {
34            mount_path: "/workspace".to_string(),
35            size: None,
36        }
37    }
38}
39
40/// A compiled workflow definition ready for execution.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct WorkflowDefinition {
43    /// Stable slug used as the primary key (e.g. "ci", "checkout"). Must be
44    /// unique within a host. Referenced by other workflows, webhooks, and
45    /// clients when starting new instances.
46    pub id: String,
47    /// Optional human-friendly display name surfaced in UIs, listings, and
48    /// logs (e.g. "Continuous Integration"). Falls back to `id` when unset.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub name: Option<String>,
51    /// Version.
52    pub version: u32,
53    /// Description.
54    pub description: Option<String>,
55    /// Steps.
56    pub steps: Vec<WorkflowStep>,
57    /// Default error behavior.
58    pub default_error_behavior: ErrorBehavior,
59    #[serde(default, with = "super::option_duration_millis")]
60    /// Default error retry interval.
61    pub default_error_retry_interval: Option<Duration>,
62    /// Infrastructure services required by this workflow (databases, caches, etc.).
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub services: Vec<ServiceDefinition>,
65    /// When set, the backend provisions a single persistent volume for the
66    /// top-level workflow instance and mounts it on every step container.
67    /// All sub-workflows inherit the same volume through their shared
68    /// namespace/isolation domain. Sub-workflow declarations are ignored.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub shared_volume: Option<SharedVolume>,
71}
72
73impl WorkflowDefinition {
74    pub fn new(id: impl Into<String>, version: u32) -> Self {
75        Self {
76            id: id.into(),
77            name: None,
78            version,
79            description: None,
80            steps: Vec::new(),
81            default_error_behavior: ErrorBehavior::default(),
82            default_error_retry_interval: None,
83            services: Vec::new(),
84            shared_volume: None,
85        }
86    }
87
88    /// Return the display name when set, otherwise fall back to the slug id.
89    pub fn display_name(&self) -> &str {
90        self.name.as_deref().unwrap_or(&self.id)
91    }
92}
93
94/// A single step in a workflow definition.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct WorkflowStep {
97    /// Id.
98    pub id: usize,
99    /// Name.
100    pub name: Option<String>,
101    /// External id.
102    pub external_id: Option<String>,
103    /// Step type.
104    pub step_type: String,
105    /// Children.
106    pub children: Vec<usize>,
107    /// Outcomes.
108    pub outcomes: Vec<StepOutcome>,
109    /// Error behavior.
110    pub error_behavior: Option<ErrorBehavior>,
111    /// Compensation step id.
112    pub compensation_step_id: Option<usize>,
113    /// Do compensate.
114    pub do_compensate: bool,
115    #[serde(default)]
116    /// Saga.
117    pub saga: bool,
118    /// Serializable configuration for primitive steps (e.g. event_name, duration).
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub step_config: Option<serde_json::Value>,
121    /// Optional condition that must evaluate to true for this step to execute.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub when: Option<StepCondition>,
124}
125
126impl WorkflowStep {
127    pub fn new(id: usize, step_type: impl Into<String>) -> Self {
128        Self {
129            id,
130            name: None,
131            external_id: None,
132            step_type: step_type.into(),
133            children: Vec::new(),
134            outcomes: Vec::new(),
135            error_behavior: None,
136            compensation_step_id: None,
137            do_compensate: false,
138            saga: false,
139            step_config: None,
140            when: None,
141        }
142    }
143}
144
145/// Routing outcome from a step.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct StepOutcome {
148    /// Next step.
149    pub next_step: usize,
150    /// Label.
151    pub label: Option<String>,
152    /// Value.
153    pub value: Option<serde_json::Value>,
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use pretty_assertions::assert_eq;
160
161    #[test]
162    fn definition_defaults() {
163        let def = WorkflowDefinition::new("test-workflow", 1);
164        assert_eq!(def.id, "test-workflow");
165        assert_eq!(def.version, 1);
166        assert!(def.steps.is_empty());
167        assert_eq!(def.default_error_behavior, ErrorBehavior::default());
168        assert!(def.default_error_retry_interval.is_none());
169    }
170
171    #[test]
172    fn step_defaults() {
173        let step = WorkflowStep::new(0, "MyStep");
174        assert_eq!(step.id, 0);
175        assert_eq!(step.step_type, "MyStep");
176        assert!(step.children.is_empty());
177        assert!(step.outcomes.is_empty());
178        assert!(step.error_behavior.is_none());
179        assert!(step.compensation_step_id.is_none());
180    }
181
182    #[test]
183    fn definition_serde_round_trip() {
184        let mut def = WorkflowDefinition::new("wf", 3);
185        let mut step = WorkflowStep::new(0, "StepA");
186        step.outcomes.push(StepOutcome {
187            next_step: 1,
188            label: Some("next".into()),
189            value: None,
190        });
191        def.steps.push(step);
192        def.steps.push(WorkflowStep::new(1, "StepB"));
193
194        let json = serde_json::to_string(&def).unwrap();
195        let deserialized: WorkflowDefinition = serde_json::from_str(&json).unwrap();
196        assert_eq!(def.id, deserialized.id);
197        assert_eq!(def.steps.len(), deserialized.steps.len());
198        assert_eq!(def.steps[0].outcomes[0].next_step, 1);
199    }
200}