1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6#[serde(rename_all = "camelCase")]
7pub struct WorkflowDispatchInput {
8 pub description: String,
10 pub required: Option<bool>,
12 pub default: Option<String>,
14 #[serde(rename = "type")]
16 pub input_type: Option<String>,
17 pub options: Option<Vec<String>>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(untagged)]
24pub enum ManualTrigger {
25 Enabled(bool),
27 WithInputs(HashMap<String, WorkflowDispatchInput>),
29}
30
31impl ManualTrigger {
32 pub fn is_enabled(&self) -> bool {
34 match self {
35 ManualTrigger::Enabled(enabled) => *enabled,
36 ManualTrigger::WithInputs(inputs) => !inputs.is_empty(),
37 }
38 }
39
40 pub fn inputs(&self) -> Option<&HashMap<String, WorkflowDispatchInput>> {
42 match self {
43 ManualTrigger::Enabled(_) => None,
44 ManualTrigger::WithInputs(inputs) => Some(inputs),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50#[serde(rename_all = "camelCase")]
51pub struct PipelineCondition {
52 pub pull_request: Option<bool>,
53 #[serde(default)]
54 pub branch: Option<StringOrVec>,
55 #[serde(default)]
56 pub tag: Option<StringOrVec>,
57 pub default_branch: Option<bool>,
58 #[serde(default)]
60 pub scheduled: Option<StringOrVec>,
61 pub manual: Option<ManualTrigger>,
63 pub release: Option<Vec<String>>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
69#[serde(rename_all = "camelCase")]
70pub struct GitHubConfig {
71 pub runner: Option<StringOrVec>,
73 pub cachix: Option<CachixConfig>,
75 pub artifacts: Option<ArtifactsConfig>,
77 pub paths_ignore: Option<Vec<String>>,
79 pub permissions: Option<HashMap<String, String>>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85#[serde(rename_all = "camelCase")]
86pub struct CachixConfig {
87 pub name: String,
89 pub auth_token: Option<String>,
91 pub push_filter: Option<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
97#[serde(rename_all = "camelCase")]
98pub struct ArtifactsConfig {
99 pub paths: Option<Vec<String>>,
101 pub if_no_files_found: Option<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
107#[serde(rename_all = "camelCase")]
108pub struct BuildkiteConfig {
109 pub queue: Option<String>,
111 pub use_emojis: Option<bool>,
113 pub plugins: Option<Vec<BuildkitePlugin>>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119pub struct BuildkitePlugin {
120 pub name: String,
122 pub config: Option<serde_json::Value>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
128#[serde(rename_all = "camelCase")]
129pub struct GitLabConfig {
130 pub image: Option<String>,
132 pub tags: Option<Vec<String>>,
134 pub cache: Option<GitLabCacheConfig>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
140pub struct GitLabCacheConfig {
141 pub key: Option<String>,
143 pub paths: Option<Vec<String>>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
149pub struct ProviderConfig {
150 pub github: Option<GitHubConfig>,
152 pub buildkite: Option<BuildkiteConfig>,
154 pub gitlab: Option<GitLabConfig>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
159#[serde(rename_all = "camelCase")]
160pub struct Pipeline {
161 pub name: String,
162 pub environment: Option<String>,
164 pub when: Option<PipelineCondition>,
165 pub tasks: Vec<String>,
166 pub derive_paths: Option<bool>,
169 pub provider: Option<ProviderConfig>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
174pub struct CI {
175 pub pipelines: Vec<Pipeline>,
176 pub provider: Option<ProviderConfig>,
178}
179
180impl CI {
181 pub fn github_config_for_pipeline(&self, pipeline_name: &str) -> GitHubConfig {
184 let global = self
185 .provider
186 .as_ref()
187 .and_then(|p| p.github.as_ref())
188 .cloned()
189 .unwrap_or_default();
190
191 let pipeline_config = self
192 .pipelines
193 .iter()
194 .find(|p| p.name == pipeline_name)
195 .and_then(|p| p.provider.as_ref())
196 .and_then(|p| p.github.as_ref());
197
198 match pipeline_config {
199 Some(pipeline) => GitHubConfig {
200 runner: pipeline.runner.clone().or(global.runner),
201 cachix: pipeline.cachix.clone().or(global.cachix),
202 artifacts: pipeline.artifacts.clone().or(global.artifacts),
203 paths_ignore: pipeline.paths_ignore.clone().or(global.paths_ignore),
204 permissions: pipeline.permissions.clone().or(global.permissions),
205 },
206 None => global,
207 }
208 }
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
212#[serde(untagged)]
213pub enum StringOrVec {
214 String(String),
215 Vec(Vec<String>),
216}
217
218impl StringOrVec {
219 pub fn to_vec(&self) -> Vec<String> {
221 match self {
222 StringOrVec::String(s) => vec![s.clone()],
223 StringOrVec::Vec(v) => v.clone(),
224 }
225 }
226
227 pub fn as_single(&self) -> Option<&str> {
229 match self {
230 StringOrVec::String(s) => Some(s),
231 StringOrVec::Vec(v) => v.first().map(|s| s.as_str()),
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_github_config_merge() {
242 let ci = CI {
243 provider: Some(ProviderConfig {
244 github: Some(GitHubConfig {
245 runner: Some(StringOrVec::String("ubuntu-latest".to_string())),
246 cachix: Some(CachixConfig {
247 name: "my-cache".to_string(),
248 auth_token: None,
249 push_filter: None,
250 }),
251 ..Default::default()
252 }),
253 ..Default::default()
254 }),
255 pipelines: vec![
256 Pipeline {
257 name: "ci".to_string(),
258 environment: None,
259 when: None,
260 tasks: vec!["test".to_string()],
261 derive_paths: None,
262 provider: Some(ProviderConfig {
263 github: Some(GitHubConfig {
264 runner: Some(StringOrVec::String("self-hosted".to_string())),
265 ..Default::default()
266 }),
267 ..Default::default()
268 }),
269 },
270 Pipeline {
271 name: "release".to_string(),
272 environment: None,
273 when: None,
274 tasks: vec!["deploy".to_string()],
275 derive_paths: None,
276 provider: None,
277 },
278 ],
279 };
280
281 let ci_config = ci.github_config_for_pipeline("ci");
283 assert_eq!(
284 ci_config.runner,
285 Some(StringOrVec::String("self-hosted".to_string()))
286 );
287 assert!(ci_config.cachix.is_some()); let release_config = ci.github_config_for_pipeline("release");
291 assert_eq!(
292 release_config.runner,
293 Some(StringOrVec::String("ubuntu-latest".to_string()))
294 );
295 }
296
297 #[test]
298 fn test_string_or_vec() {
299 let single = StringOrVec::String("value".to_string());
300 assert_eq!(single.to_vec(), vec!["value"]);
301 assert_eq!(single.as_single(), Some("value"));
302
303 let multi = StringOrVec::Vec(vec!["a".to_string(), "b".to_string()]);
304 assert_eq!(multi.to_vec(), vec!["a", "b"]);
305 assert_eq!(multi.as_single(), Some("a"));
306 }
307
308 #[test]
309 fn test_manual_trigger_bool() {
310 let json = r#"{"manual": true}"#;
311 let cond: PipelineCondition = serde_json::from_str(json).unwrap();
312 assert!(matches!(cond.manual, Some(ManualTrigger::Enabled(true))));
313
314 let json = r#"{"manual": false}"#;
315 let cond: PipelineCondition = serde_json::from_str(json).unwrap();
316 assert!(matches!(cond.manual, Some(ManualTrigger::Enabled(false))));
317 }
318
319 #[test]
320 fn test_manual_trigger_with_inputs() {
321 let json =
322 r#"{"manual": {"tag_name": {"description": "Tag to release", "required": true}}}"#;
323 let cond: PipelineCondition = serde_json::from_str(json).unwrap();
324
325 match &cond.manual {
326 Some(ManualTrigger::WithInputs(inputs)) => {
327 assert!(inputs.contains_key("tag_name"));
328 let input = inputs.get("tag_name").unwrap();
329 assert_eq!(input.description, "Tag to release");
330 assert_eq!(input.required, Some(true));
331 }
332 _ => panic!("Expected WithInputs variant"),
333 }
334 }
335
336 #[test]
337 fn test_manual_trigger_helpers() {
338 let enabled = ManualTrigger::Enabled(true);
339 assert!(enabled.is_enabled());
340 assert!(enabled.inputs().is_none());
341
342 let disabled = ManualTrigger::Enabled(false);
343 assert!(!disabled.is_enabled());
344
345 let mut inputs = HashMap::new();
346 inputs.insert(
347 "tag".to_string(),
348 WorkflowDispatchInput {
349 description: "Tag name".to_string(),
350 required: Some(true),
351 default: None,
352 input_type: None,
353 options: None,
354 },
355 );
356 let with_inputs = ManualTrigger::WithInputs(inputs);
357 assert!(with_inputs.is_enabled());
358 assert!(with_inputs.inputs().is_some());
359 }
360
361 #[test]
362 fn test_scheduled_cron_expressions() {
363 let json = r#"{"scheduled": "0 0 * * 0"}"#;
365 let cond: PipelineCondition = serde_json::from_str(json).unwrap();
366 match &cond.scheduled {
367 Some(StringOrVec::String(s)) => assert_eq!(s, "0 0 * * 0"),
368 _ => panic!("Expected single string"),
369 }
370
371 let json = r#"{"scheduled": ["0 0 * * 0", "0 12 * * *"]}"#;
373 let cond: PipelineCondition = serde_json::from_str(json).unwrap();
374 match &cond.scheduled {
375 Some(StringOrVec::Vec(v)) => {
376 assert_eq!(v.len(), 2);
377 assert_eq!(v[0], "0 0 * * 0");
378 assert_eq!(v[1], "0 12 * * *");
379 }
380 _ => panic!("Expected vec"),
381 }
382 }
383
384 #[test]
385 fn test_release_trigger() {
386 let json = r#"{"release": ["published", "created"]}"#;
387 let cond: PipelineCondition = serde_json::from_str(json).unwrap();
388 assert_eq!(
389 cond.release,
390 Some(vec!["published".to_string(), "created".to_string()])
391 );
392 }
393
394 #[test]
395 fn test_pipeline_derive_paths() {
396 let json = r#"{"name": "ci", "tasks": ["test"], "derivePaths": true}"#;
397 let pipeline: Pipeline = serde_json::from_str(json).unwrap();
398 assert_eq!(pipeline.derive_paths, Some(true));
399
400 let json = r#"{"name": "scheduled", "tasks": ["sync"], "derivePaths": false}"#;
401 let pipeline: Pipeline = serde_json::from_str(json).unwrap();
402 assert_eq!(pipeline.derive_paths, Some(false));
403
404 let json = r#"{"name": "default", "tasks": ["build"]}"#;
405 let pipeline: Pipeline = serde_json::from_str(json).unwrap();
406 assert_eq!(pipeline.derive_paths, None);
407 }
408}