1use crate::schema::{SchemaType, SchemaValidator};
2use crate::workflow;
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6use thiserror::Error;
7use wrkflw_models::gitlab::Pipeline;
8use wrkflw_models::ValidationResult;
9
10#[derive(Error, Debug)]
11pub enum GitlabParserError {
12 #[error("I/O error: {0}")]
13 IoError(#[from] std::io::Error),
14
15 #[error("YAML parsing error: {0}")]
16 YamlError(#[from] serde_yaml::Error),
17
18 #[error("Invalid pipeline structure: {0}")]
19 InvalidStructure(String),
20
21 #[error("Schema validation error: {0}")]
22 SchemaValidationError(String),
23}
24
25pub fn parse_pipeline(pipeline_path: &Path) -> Result<Pipeline, GitlabParserError> {
27 let pipeline_content = fs::read_to_string(pipeline_path)?;
29
30 let validator = SchemaValidator::new().map_err(GitlabParserError::SchemaValidationError)?;
32
33 validator
34 .validate_with_specific_schema(&pipeline_content, SchemaType::GitLab)
35 .map_err(GitlabParserError::SchemaValidationError)?;
36
37 let pipeline: Pipeline = serde_yaml::from_str(&pipeline_content)?;
39
40 Ok(pipeline)
42}
43
44pub fn validate_pipeline_structure(pipeline: &Pipeline) -> ValidationResult {
46 let mut result = ValidationResult::new();
47
48 if pipeline.jobs.is_empty() {
50 result.add_issue("Pipeline must contain at least one job".to_string());
51 }
52
53 for (job_name, job) in &pipeline.jobs {
55 if let Some(true) = job.template {
57 continue;
58 }
59
60 if job.script.is_none() && job.extends.is_none() {
62 result.add_issue(format!(
63 "Job '{}' must have a script section or extend another job",
64 job_name
65 ));
66 }
67 }
68
69 if let Some(stages) = &pipeline.stages {
71 for (job_name, job) in &pipeline.jobs {
72 if let Some(stage) = &job.stage {
73 if !stages.contains(stage) {
74 result.add_issue(format!(
75 "Job '{}' references undefined stage '{}'",
76 job_name, stage
77 ));
78 }
79 }
80 }
81 }
82
83 for (job_name, job) in &pipeline.jobs {
85 if let Some(dependencies) = &job.dependencies {
86 for dependency in dependencies {
87 if !pipeline.jobs.contains_key(dependency) {
88 result.add_issue(format!(
89 "Job '{}' depends on undefined job '{}'",
90 job_name, dependency
91 ));
92 }
93 }
94 }
95 }
96
97 for (job_name, job) in &pipeline.jobs {
99 if let Some(extends) = &job.extends {
100 for extend in extends {
101 if !pipeline.jobs.contains_key(extend) {
102 result.add_issue(format!(
103 "Job '{}' extends undefined job '{}'",
104 job_name, extend
105 ));
106 }
107 }
108 }
109 }
110
111 result
112}
113
114pub fn convert_to_workflow_format(pipeline: &Pipeline) -> workflow::WorkflowDefinition {
116 let mut workflow = workflow::WorkflowDefinition {
118 name: "Converted GitLab CI Pipeline".to_string(),
119 on: vec!["push".to_string()], on_raw: serde_yaml::Value::String("push".to_string()),
121 jobs: HashMap::new(),
122 };
123
124 for (job_name, gitlab_job) in &pipeline.jobs {
126 if let Some(true) = gitlab_job.template {
128 continue;
129 }
130
131 let mut job = workflow::Job {
133 runs_on: Some(vec!["ubuntu-latest".to_string()]), needs: None,
135 steps: Vec::new(),
136 env: HashMap::new(),
137 matrix: None,
138 services: HashMap::new(),
139 if_condition: None,
140 outputs: None,
141 permissions: None,
142 uses: None,
143 with: None,
144 secrets: None,
145 };
146
147 if let Some(variables) = &gitlab_job.variables {
149 job.env.extend(variables.clone());
150 }
151
152 if let Some(variables) = &pipeline.variables {
154 for (key, value) in variables {
156 job.env.entry(key.clone()).or_insert_with(|| value.clone());
157 }
158 }
159
160 if let Some(before_script) = &gitlab_job.before_script {
162 for (i, cmd) in before_script.iter().enumerate() {
163 let step = workflow::Step {
164 name: Some(format!("Before script {}", i + 1)),
165 uses: None,
166 run: Some(cmd.clone()),
167 with: None,
168 env: HashMap::new(),
169 continue_on_error: None,
170 };
171 job.steps.push(step);
172 }
173 }
174
175 if let Some(script) = &gitlab_job.script {
177 for (i, cmd) in script.iter().enumerate() {
178 let step = workflow::Step {
179 name: Some(format!("Run script line {}", i + 1)),
180 uses: None,
181 run: Some(cmd.clone()),
182 with: None,
183 env: HashMap::new(),
184 continue_on_error: None,
185 };
186 job.steps.push(step);
187 }
188 }
189
190 if let Some(after_script) = &gitlab_job.after_script {
192 for (i, cmd) in after_script.iter().enumerate() {
193 let step = workflow::Step {
194 name: Some(format!("After script {}", i + 1)),
195 uses: None,
196 run: Some(cmd.clone()),
197 with: None,
198 env: HashMap::new(),
199 continue_on_error: Some(true), };
201 job.steps.push(step);
202 }
203 }
204
205 if let Some(services) = &gitlab_job.services {
207 for (i, service) in services.iter().enumerate() {
208 let service_name = format!("service-{}", i);
209 let service_image = match service {
210 wrkflw_models::gitlab::Service::Simple(name) => name.clone(),
211 wrkflw_models::gitlab::Service::Detailed { name, .. } => name.clone(),
212 };
213
214 let service = workflow::Service {
215 image: service_image,
216 ports: None,
217 env: HashMap::new(),
218 volumes: None,
219 options: None,
220 };
221
222 job.services.insert(service_name, service);
223 }
224 }
225
226 workflow.jobs.insert(job_name.clone(), job);
228 }
229
230 workflow
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use tempfile::NamedTempFile;
238
239 #[test]
240 fn test_parse_simple_pipeline() {
241 let file = NamedTempFile::new().unwrap();
243 let content = r#"
244stages:
245 - build
246 - test
247
248build_job:
249 stage: build
250 script:
251 - echo "Building..."
252 - make build
253
254test_job:
255 stage: test
256 script:
257 - echo "Testing..."
258 - make test
259"#;
260 fs::write(&file, content).unwrap();
261
262 let pipeline = parse_pipeline(&file.path()).unwrap();
264
265 assert_eq!(pipeline.stages.as_ref().unwrap().len(), 2);
267 assert_eq!(pipeline.jobs.len(), 2);
268
269 let build_job = pipeline.jobs.get("build_job").unwrap();
271 assert_eq!(build_job.stage.as_ref().unwrap(), "build");
272 assert_eq!(build_job.script.as_ref().unwrap().len(), 2);
273
274 let test_job = pipeline.jobs.get("test_job").unwrap();
275 assert_eq!(test_job.stage.as_ref().unwrap(), "test");
276 assert_eq!(test_job.script.as_ref().unwrap().len(), 2);
277 }
278}