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