1use std::fmt::{self, Display, Formatter, Write};
8
9use crate::err_msg_parse::ErrorMessageSummary;
10
11#[derive(Debug)]
12pub struct Issue {
13 title: String,
14 labels: Vec<String>,
15 body: IssueBody,
16}
17
18impl Issue {
19 pub fn new(
20 run_id: String,
21 run_link: String,
22 failed_jobs: Vec<FailedJob>,
23 label: String,
24 ) -> Self {
25 let mut labels = vec![label];
26 failed_jobs.iter().for_each(|job| {
27 if let Some(failure_label) = job.failure_label() {
28 log::debug!("Adding failure label {failure_label} to issue");
29 labels.push(failure_label);
30 }
31 });
32 Self {
33 title: "Scheduled run failed".to_string(),
34 labels,
35 body: IssueBody::new(run_id, run_link, failed_jobs),
36 }
37 }
38
39 pub fn title(&self) -> &str {
40 self.title.as_str()
41 }
42
43 pub fn labels(&self) -> &[String] {
44 self.labels.as_slice()
45 }
46
47 pub fn body(&self) -> String {
48 self.body.to_string()
49 }
50}
51
52#[derive(Debug)]
53pub struct IssueBody {
54 run_id: String,
55 run_link: String,
56 failed_jobs: Vec<FailedJob>,
57}
58
59impl IssueBody {
60 pub fn new(run_id: String, run_link: String, failed_jobs: Vec<FailedJob>) -> Self {
61 Self {
62 run_id,
63 run_link,
64 failed_jobs,
65 }
66 }
67}
68
69impl Display for IssueBody {
70 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
71 write!(
72 f,
73 "**Run ID**: {id} [LINK TO RUN]({run_url})
74
75**{failed_jobs_list_title}**
76{failed_jobs_name_list}",
77 id = self.run_id,
78 run_url = self.run_link,
79 failed_jobs_list_title = format_args!(
80 "{cnt} {job} failed:",
81 cnt = self.failed_jobs.len(),
82 job = if self.failed_jobs.len() == 1 {
83 "job"
84 } else {
85 "jobs"
86 }
87 ),
88 failed_jobs_name_list =
89 self.failed_jobs
90 .iter()
91 .fold(String::new(), |mut s_out, job| {
92 let _ = writeln!(s_out, "- **`{}`**", job.name);
93 s_out
94 })
95 )?;
96 for job in &self.failed_jobs {
97 write!(f, "{job}")?;
98 }
99 Ok(())
100 }
101}
102
103#[derive(Debug)]
104pub struct FailedJob {
105 name: String,
106 id: String,
107 url: String,
108 failed_step: String,
109 error_message: ErrorMessageSummary,
110}
111
112impl FailedJob {
113 pub fn new(
114 name: String,
115 id: String,
116 url: String,
117 failed_step: String,
118 error_message: ErrorMessageSummary,
119 ) -> Self {
120 Self {
121 name,
122 id,
123 url,
124 failed_step,
125 error_message,
126 }
127 }
128
129 pub fn failure_label(&self) -> Option<String> {
130 self.error_message.failure_label()
131 }
132}
133
134impl Display for FailedJob {
135 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
136 let summary = self.error_message.summary();
137 let optional_log = match (self.error_message.logfile_name(), self.error_message.log()) {
138 (Some(name), Some(contents)) => format!(
139 "
140<details>
141<summary>{name}</summary>
142<br>
143
144```
145{contents}
146```
147</details>"
148 ),
149 _ => String::from(""),
150 };
151
152 write!(
153 f,
154 "
155### `{name}` (ID {id})
156**Step failed:** `{failed_step}`
157\\
158**Log:** {url}
159\\
160*Best effort error summary*:
161```
162{error_message}```{optional_log}",
163 name = self.name,
164 id = self.id,
165 failed_step = self.failed_step,
166 url = self.url,
167 error_message = summary,
168 optional_log = optional_log
169 )
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use pretty_assertions::assert_eq;
177
178 const EXAMPLE_ISSUE_BODY: &str = r#"**Run ID**: 7858139663 [LINK TO RUN]( https://github.com/luftkode/distro-template/actions/runs/7850874958)
179
180**2 jobs failed:**
181- **`Test template xilinx`**
182- **`Test template raspberry`**
183
184### `Test template xilinx` (ID 21442749267)
185**Step failed:** `📦 Build yocto image`
186\
187**Log:** https://github.com/luftkode/distro-template/actions/runs/7850874958/job/21442749267
188\
189*Best effort error summary*:
190```
191Yocto error: ERROR: No recipes available for: ...
192```
193### `Test template raspberry` (ID 21442749166)
194**Step failed:** `📦 Build yocto image`
195\
196**Log:** https://github.com/luftkode/distro-template/actions/runs/7850874958/job/21442749166
197\
198*Best effort error summary*:
199```
200Yocto error: ERROR: No recipes available for: ...
201```"#;
202
203 #[test]
204 fn test_issue_new() {
205 let run_id = "7858139663".to_string();
206 let run_link =
207 "https://github.com/luftkode/distro-template/actions/runs/7850874958".to_string();
208 let failed_jobs = vec![
209 FailedJob::new(
210 "Test template xilinx".to_string(),
211 "21442749267".to_string(),
212 "https://github.com/luftkode/distro-template/actions/runs/7850874958/job/21442749267".to_string(),
213 "📦 Build yocto image".to_string(),
214 ErrorMessageSummary::Other("Yocto error: ERROR: No recipes available for: ...
215".to_string()),
216 ),
217 FailedJob::new(
218 "Test template raspberry".to_string(),
219 "21442749166".to_string(),
220 "https://github.com/luftkode/distro-template/actions/runs/7850874958/job/21442749166".to_string(),
221 "📦 Build yocto image".to_string(),
222 ErrorMessageSummary::Other("Yocto error: ERROR: No recipes available for: ...
223".to_string()),
224 ),
225 ];
226 let label = "bug".to_string();
227 let issue = Issue::new(run_id, run_link, failed_jobs, label);
228 assert_eq!(issue.title, "Scheduled run failed");
229 assert_eq!(issue.labels, ["bug"]);
230 assert_eq!(issue.body.failed_jobs.len(), 2);
231 assert_eq!(issue.body.failed_jobs[0].id, "21442749267");
232 }
233
234 #[test]
235 fn test_issue_body_display() {
236 let run_id = "7858139663".to_string();
237 let run_link =
238 " https://github.com/luftkode/distro-template/actions/runs/7850874958".to_string();
239 let failed_jobs = vec![
240 FailedJob::new(
241 "Test template xilinx".to_string(),
242 "21442749267".to_string(),
243 "https://github.com/luftkode/distro-template/actions/runs/7850874958/job/21442749267".to_string(),
244 "📦 Build yocto image".to_string(),
245 ErrorMessageSummary::Other("Yocto error: ERROR: No recipes available for: ...
246".to_string()),
247 ),
248 FailedJob::new(
249 "Test template raspberry".to_string(),
250 "21442749166".to_string(),
251 "https://github.com/luftkode/distro-template/actions/runs/7850874958/job/21442749166".to_string(),
252 "📦 Build yocto image".to_string(),
253 ErrorMessageSummary::Other("Yocto error: ERROR: No recipes available for: ...
254".to_string()),
255 ),
256 ];
257
258 let issue_body = IssueBody::new(run_id, run_link, failed_jobs);
259 assert_eq!(issue_body.to_string(), EXAMPLE_ISSUE_BODY);
260 }
262}