1use std::path::PathBuf;
5use std::process::Command;
6
7use derive_setters::Setters;
8use indexmap::IndexMap;
9
10use crate::error::{Error, Result};
11use crate::{Job, Jobs, Workflow};
12
13#[derive(Setters, Clone)]
14#[setters(into)]
15pub struct Generate {
16 workflow: Workflow,
17 name: String,
18}
19
20impl Generate {
21 pub fn new(workflow: Workflow) -> Self {
22 let workflow = organize_job_dependency(workflow);
23 Self { workflow, name: "ci.yml".to_string() }
24 }
25
26 fn check_file(&self, path: &PathBuf, content: &str) -> Result<()> {
27 if let Ok(prev) = std::fs::read_to_string(path) {
28 if content != prev {
29 Err(Error::OutdatedWorkflow)
30 } else {
31 Ok(())
32 }
33 } else {
34 Err(Error::MissingWorkflowFile(path.clone()))
35 }
36 }
37
38 pub fn generate(&self) -> Result<()> {
39 let comment = include_str!("./comment.yml");
40
41 let root_dir = String::from_utf8(
42 Command::new("git")
43 .args(["rev-parse", "--show-toplevel"])
44 .output()?
45 .stdout,
46 )?;
47
48 let path = PathBuf::from(root_dir.trim())
49 .join(".github")
50 .join("workflows")
51 .join(self.name.as_str());
52
53 let content = format!("{}\n{}", comment, self.workflow.to_string()?);
54
55 let result = self.check_file(&path, &content);
56
57 if std::env::var("CI").is_ok() {
58 result
59 } else {
60 match result {
61 Ok(()) => {
62 println!("Workflow file is up-to-date: {}", path.display());
63 Ok(())
64 }
65 Err(Error::OutdatedWorkflow) => {
66 std::fs::write(path.clone(), content)?;
67 println!("Updated workflow file: {}", path.display());
68 Ok(())
69 }
70 Err(Error::MissingWorkflowFile(path)) => {
71 std::fs::create_dir_all(path.parent().ok_or(Error::IO(
72 std::io::Error::other("Invalid parent dir(s) path"),
73 ))?)?;
74 std::fs::write(path.clone(), content)?;
75 println!("Generated workflow file: {}", path.display());
76 Ok(())
77 }
78 Err(e) => Err(e),
79 }
80 }
81 }
82}
83
84fn organize_job_dependency(mut workflow: Workflow) -> Workflow {
93 let mut job_id = 0;
94 let mut new_jobs = IndexMap::<String, Job>::new();
95 let empty_map = IndexMap::default();
96
97 let old_jobs: &IndexMap<String, Job> = workflow
98 .jobs
99 .as_ref()
100 .map(|jobs| &jobs.0)
101 .unwrap_or(&empty_map);
102
103 for (id, mut job) in workflow.jobs.clone().unwrap_or_default().0.into_iter() {
105 if let Some(dep_jobs) = &job.tmp_needs {
107 let mut job_ids = Vec::<String>::new();
109 for job in dep_jobs.iter() {
110 if let Some(id) = find_value(job, &new_jobs).or(find_value(job, old_jobs)) {
112 job_ids.push(id.to_owned());
113 } else {
114 let id = format!("job-{job_id}");
116
117 job_ids.push(id.clone());
119
120 new_jobs.insert(format!("job-{job_id}"), job.clone());
122
123 job_id += 1;
124 }
125 }
126 job.needs = Some(job_ids);
127 }
128
129 new_jobs.insert(id.clone(), job.clone());
130 }
131
132 workflow.jobs = Some(Jobs(new_jobs));
133
134 workflow
135}
136
137fn find_value<'a, K, V: PartialEq>(job: &V, map: &'a IndexMap<K, V>) -> Option<&'a K> {
139 map.iter()
140 .find_map(|(k, v)| if v == job { Some(k) } else { None })
141}
142
143#[cfg(test)]
144mod tests {
145 use insta::assert_snapshot;
146
147 use super::*;
148
149 #[test]
150 fn add_needs_job() {
151 let base_job = Job::new("Base job");
152
153 let job1 =
154 Job::new("The first job that has dependency for base_job").add_needs(base_job.clone());
155 let job2 =
156 Job::new("The second job that has dependency for base_job").add_needs(base_job.clone());
157
158 let workflow = Workflow::new("All jobs were added to workflow")
159 .add_job("base_job", base_job)
160 .add_job("with-dependency-1", job1)
161 .add_job("with-dependency-2", job2);
162
163 let workflow = Generate::new(workflow).workflow;
164
165 assert_snapshot!(workflow.to_string().unwrap());
166 }
167
168 #[test]
169 fn missing_add_job() {
170 let base_job = Job::new("Base job");
171
172 let job1 =
173 Job::new("The first job that has dependency for base_job").add_needs(base_job.clone());
174 let job2 =
175 Job::new("The second job that has dependency for base_job").add_needs(base_job.clone());
176
177 let workflow = Workflow::new("base_job was not added to workflow jobs")
178 .add_job("with-dependency-1", job1)
179 .add_job("with-dependency-2", job2);
180
181 let workflow = Generate::new(workflow).workflow;
182
183 assert_snapshot!(workflow.to_string().unwrap());
184 }
185}