devops_models/models/
gitlab.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct GitLabArtifacts {
6 #[serde(default)]
7 pub paths: Vec<String>,
8 #[serde(default)]
9 pub expire_in: Option<String>,
10 #[serde(default)]
11 pub when: Option<String>,
12 #[serde(default)]
13 pub reports: Option<serde_json::Value>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct GitLabCache {
18 #[serde(default)]
19 pub key: Option<String>,
20 #[serde(default)]
21 pub paths: Vec<String>,
22 #[serde(default)]
23 pub policy: Option<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct GitLabRule {
28 #[serde(default, rename = "if")]
29 pub condition: Option<String>,
30 #[serde(default)]
31 pub when: Option<String>,
32 #[serde(default)]
33 pub changes: Option<Vec<String>>,
34 #[serde(default)]
35 pub exists: Option<Vec<String>>,
36 #[serde(default)]
37 pub allow_failure: Option<bool>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct GitLabJob {
42 #[serde(default)]
43 pub stage: Option<String>,
44 #[serde(default)]
45 pub image: Option<String>,
46 #[serde(default)]
47 pub script: Vec<String>,
48 #[serde(default)]
49 pub before_script: Vec<String>,
50 #[serde(default)]
51 pub after_script: Vec<String>,
52 #[serde(default)]
53 pub artifacts: Option<GitLabArtifacts>,
54 #[serde(default)]
55 pub cache: Option<GitLabCache>,
56 #[serde(default)]
57 pub rules: Vec<GitLabRule>,
58 #[serde(default)]
59 pub needs: Vec<String>,
60 #[serde(default)]
61 pub tags: Vec<String>,
62 #[serde(default)]
63 pub variables: HashMap<String, String>,
64 #[serde(default)]
65 pub allow_failure: Option<bool>,
66 #[serde(default)]
67 pub retry: Option<serde_json::Value>,
68 #[serde(default)]
69 pub timeout: Option<String>,
70 #[serde(default)]
71 pub services: Vec<serde_json::Value>,
72 #[serde(default)]
73 pub only: Option<serde_json::Value>,
74 #[serde(default)]
75 pub except: Option<serde_json::Value>,
76 #[serde(default)]
77 pub environment: Option<serde_json::Value>,
78 #[serde(default)]
79 pub coverage: Option<String>,
80 #[serde(default)]
81 pub when: Option<String>,
82 #[serde(default)]
83 pub extends: Option<serde_json::Value>,
84 #[serde(default)]
85 pub dependencies: Option<Vec<String>>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct GitLabCI {
92 #[serde(default)]
93 pub stages: Vec<String>,
94 #[serde(default)]
95 pub image: Option<String>,
96 #[serde(default)]
97 pub variables: HashMap<String, serde_json::Value>,
98 #[serde(default)]
99 pub default: Option<serde_json::Value>,
100 #[serde(default)]
101 pub include: Option<serde_json::Value>,
102 #[serde(default)]
103 pub workflow: Option<serde_json::Value>,
104 #[serde(skip)]
106 pub jobs: HashMap<String, GitLabJob>,
107}
108
109const RESERVED_KEYWORDS: &[&str] = &[
110 "stages", "image", "variables", "default", "include", "workflow",
111 "before_script", "after_script", "cache", "services",
112];
113
114impl GitLabCI {
115 pub fn from_value(value: &serde_json::Value) -> Result<Self, String> {
118 let obj = value.as_object().ok_or("GitLab CI must be a YAML mapping")?;
119
120 let mut ci: GitLabCI = serde_json::from_value(value.clone())
121 .map_err(|e| format!("Failed to parse GitLab CI config: {e}"))?;
122
123 for (key, val) in obj {
125 if !RESERVED_KEYWORDS.contains(&key.as_str()) && !key.starts_with('.') {
126 match serde_json::from_value::<GitLabJob>(val.clone()) {
127 Ok(job) => { ci.jobs.insert(key.clone(), job); }
128 Err(e) => return Err(format!("Invalid job '{key}': {e}")),
129 }
130 }
131 }
132
133 Ok(ci)
134 }
135
136 pub fn validate(&self) -> Vec<String> {
138 let mut warnings = Vec::new();
139
140 for (name, job) in &self.jobs {
142 if let Some(stage) = &job.stage
143 && !self.stages.is_empty()
144 && !self.stages.contains(stage)
145 {
146 warnings.push(format!(
147 "Job '{}': stage '{}' not declared in stages list",
148 name, stage
149 ));
150 }
151
152 for need in &job.needs {
154 if !self.jobs.contains_key(need) {
155 warnings.push(format!(
156 "Job '{}': needs '{}' which is not defined",
157 name, need
158 ));
159 }
160 }
161
162 if job.script.is_empty() {
164 warnings.push(format!("Job '{}': script is empty", name));
165 }
166 }
167
168 if self.stages.is_empty() && !self.jobs.is_empty() {
169 warnings.push("No 'stages' defined — jobs may run in undefined order".to_string());
170 }
171
172 warnings
173 }
174}