gh_workflow_parser/
issue.rs

1//! Contains the Issue struct and its associated methods.
2//!
3//! The Issue struct is used to represent a GitHub issue that will be created
4//! in a repository. It contains a title, label, and body. The body is a
5//! collection of FailedJob structs, which contain information about the failed
6//! jobs in a GitHub Actions workflow run.
7use 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        //std::fs::write("test2.md", issue_body.to_string()).unwrap();
261    }
262}