1use serde::Serialize;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Default, Serialize)]
11pub struct Pipeline {
12 pub steps: Vec<Step>,
14
15 #[serde(skip_serializing_if = "HashMap::is_empty")]
17 pub env: HashMap<String, String>,
18}
19
20#[derive(Debug, Clone, Serialize)]
22#[serde(untagged)]
23pub enum Step {
24 Command(Box<CommandStep>),
26 Block(BlockStep),
28 Wait(WaitStep),
30 Group(GroupStep),
32}
33
34#[derive(Debug, Clone, Default, Serialize)]
36pub struct CommandStep {
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub label: Option<String>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub key: Option<String>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub command: Option<CommandValue>,
48
49 #[serde(skip_serializing_if = "HashMap::is_empty")]
51 pub env: HashMap<String, String>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub agents: Option<AgentRules>,
56
57 #[serde(skip_serializing_if = "Vec::is_empty")]
59 pub artifact_paths: Vec<String>,
60
61 #[serde(skip_serializing_if = "Vec::is_empty")]
63 pub depends_on: Vec<DependsOn>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub concurrency_group: Option<String>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub concurrency: Option<u32>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub retry: Option<RetryConfig>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub timeout_in_minutes: Option<u32>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub soft_fail: Option<bool>,
84}
85
86#[derive(Debug, Clone, Serialize)]
88#[serde(untagged)]
89pub enum CommandValue {
90 Single(String),
92 Array(Vec<String>),
94}
95
96#[derive(Debug, Clone, Serialize)]
98pub struct BlockStep {
99 pub block: String,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub key: Option<String>,
105
106 #[serde(skip_serializing_if = "Vec::is_empty")]
108 pub depends_on: Vec<DependsOn>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub prompt: Option<String>,
113
114 #[serde(skip_serializing_if = "Vec::is_empty")]
116 pub fields: Vec<BlockField>,
117}
118
119impl BlockStep {
120 pub fn new(label: impl Into<String>) -> Self {
122 Self {
123 block: label.into(),
124 key: None,
125 depends_on: Vec::new(),
126 prompt: None,
127 fields: Vec::new(),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize)]
134pub struct BlockField {
135 #[serde(rename = "type")]
137 pub field_type: String,
138
139 pub key: String,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub text: Option<String>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub required: Option<bool>,
149}
150
151#[derive(Debug, Clone, Serialize)]
153pub struct WaitStep {
154 pub wait: Option<String>,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub continue_on_failure: Option<bool>,
160}
161
162impl Default for WaitStep {
163 fn default() -> Self {
164 Self {
165 wait: Some("~".to_string()),
166 continue_on_failure: None,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize)]
173pub struct GroupStep {
174 pub group: String,
176
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub key: Option<String>,
180
181 pub steps: Vec<Step>,
183
184 #[serde(skip_serializing_if = "Vec::is_empty")]
186 pub depends_on: Vec<DependsOn>,
187}
188
189#[derive(Debug, Clone, Serialize)]
191pub struct AgentRules {
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub queue: Option<String>,
195
196 #[serde(flatten)]
198 pub tags: HashMap<String, String>,
199}
200
201impl AgentRules {
202 pub fn with_queue(queue: impl Into<String>) -> Self {
204 Self {
205 queue: Some(queue.into()),
206 tags: HashMap::new(),
207 }
208 }
209
210 #[must_use]
212 pub fn from_tags(tags: Vec<String>) -> Option<Self> {
213 if tags.is_empty() {
214 return None;
215 }
216
217 let mut rules = Self {
218 queue: None,
219 tags: HashMap::new(),
220 };
221
222 for tag in tags {
223 if let Some((key, value)) = tag.split_once('=') {
224 if key == "queue" {
225 rules.queue = Some(value.to_string());
226 } else {
227 rules.tags.insert(key.to_string(), value.to_string());
228 }
229 } else {
230 rules.queue = Some(tag);
232 }
233 }
234
235 Some(rules)
236 }
237}
238
239#[derive(Debug, Clone, Serialize)]
241#[serde(untagged)]
242pub enum DependsOn {
243 Key(String),
245 Detailed(DetailedDependency),
247}
248
249#[derive(Debug, Clone, Serialize)]
251pub struct DetailedDependency {
252 pub step: String,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub allow_failure: Option<bool>,
258}
259
260#[derive(Debug, Clone, Serialize)]
262pub struct RetryConfig {
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub automatic: Option<AutomaticRetry>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub manual: Option<ManualRetry>,
270}
271
272#[derive(Debug, Clone, Serialize)]
274#[serde(untagged)]
275pub enum AutomaticRetry {
276 Enabled(bool),
278 Config(Vec<AutomaticRetryRule>),
280}
281
282#[derive(Debug, Clone, Serialize)]
284pub struct AutomaticRetryRule {
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub exit_status: Option<String>,
288
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub limit: Option<u32>,
292}
293
294#[derive(Debug, Clone, Serialize)]
296pub struct ManualRetry {
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub allowed: Option<bool>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub permit_on_passed: Option<bool>,
304
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub reason: Option<String>,
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_command_step_serialization() {
316 let step = CommandStep {
317 label: Some(":rust: Build".to_string()),
318 key: Some("build".to_string()),
319 command: Some(CommandValue::Array(vec![
320 "cargo".to_string(),
321 "build".to_string(),
322 ])),
323 env: HashMap::from([("RUST_BACKTRACE".to_string(), "1".to_string())]),
324 ..Default::default()
325 };
326
327 let yaml = serde_yaml::to_string(&step).unwrap();
328 assert!(yaml.contains("label:"));
329 assert!(yaml.contains("key: build"));
330 assert!(yaml.contains("RUST_BACKTRACE"));
331 }
332
333 #[test]
334 fn test_block_step_serialization() {
335 let step = BlockStep::new(":hand: Approve Deploy");
336
337 let yaml = serde_yaml::to_string(&step).unwrap();
338 assert!(yaml.contains("block:"));
339 assert!(yaml.contains("Approve Deploy"));
340 }
341
342 #[test]
343 fn test_agent_rules_from_tags() {
344 let rules = AgentRules::from_tags(vec!["linux-x86".to_string()]);
345 assert!(rules.is_some());
346 assert_eq!(rules.unwrap().queue, Some("linux-x86".to_string()));
347
348 let rules = AgentRules::from_tags(vec!["queue=deploy".to_string(), "os=linux".to_string()]);
349 let rules = rules.unwrap();
350 assert_eq!(rules.queue, Some("deploy".to_string()));
351 assert_eq!(rules.tags.get("os"), Some(&"linux".to_string()));
352 }
353
354 #[test]
355 fn test_pipeline_serialization() {
356 let pipeline = Pipeline {
357 steps: vec![Step::Command(Box::new(CommandStep {
358 label: Some("Test".to_string()),
359 key: Some("test".to_string()),
360 command: Some(CommandValue::Single("echo hello".to_string())),
361 ..Default::default()
362 }))],
363 env: HashMap::new(),
364 };
365
366 let yaml = serde_yaml::to_string(&pipeline).unwrap();
367 assert!(yaml.contains("steps:"));
368 assert!(yaml.contains("label: Test"));
369 }
370}