ci_manager/ci_provider/github/
util.rs

1//! Contains the ErrorLog struct describing a failed job log from GitHub Actions.
2use octocrab::models::{
3    workflows::{Job, Step},
4    JobId,
5};
6
7use super::JobLog;
8
9#[derive(Debug)]
10pub struct JobErrorLog {
11    pub job_id: JobId,
12    pub job_name: String,
13    pub failed_step_logs: Vec<StepErrorLog>,
14}
15
16impl JobErrorLog {
17    pub fn new(job_id: JobId, job_name: String, logs: Vec<StepErrorLog>) -> Self {
18        JobErrorLog {
19            job_id,
20            job_name,
21            failed_step_logs: logs,
22        }
23    }
24
25    /// Returns the logs as a string
26    pub fn logs_as_str(&self) -> String {
27        let mut logs = String::new();
28        for log in &self.failed_step_logs {
29            logs.push_str(log.contents());
30        }
31        logs
32    }
33}
34
35#[derive(Debug)]
36pub struct StepErrorLog {
37    pub step_name: String,
38    pub contents: String,
39}
40
41impl StepErrorLog {
42    pub fn new(step_name: String, error_log: String) -> Self {
43        StepErrorLog {
44            step_name,
45            contents: error_log,
46        }
47    }
48
49    pub fn contents(&self) -> &str {
50        self.contents.as_str()
51    }
52}
53
54pub fn repo_url_to_job_url(repo_url: &str, run_id: &str, job_id: &str) -> String {
55    let run_url = repo_url_to_run_url(repo_url, run_id);
56    run_url_to_job_url(&run_url, job_id)
57}
58
59pub fn repo_url_to_run_url(repo_url: &str, run_id: &str) -> String {
60    format!("{repo_url}/actions/runs/{run_id}")
61}
62
63pub fn run_url_to_job_url(run_url: &str, job_id: &str) -> String {
64    format!("{run_url}/job/{job_id}")
65}
66
67pub fn distance_to_other_issues(
68    issue_body: &str,
69    other_issues: &[octocrab::models::issues::Issue],
70) -> usize {
71    let other_issue_bodies: Vec<String> = other_issues
72        .iter()
73        .map(|issue| issue.body.as_deref().unwrap_or_default().to_string())
74        .collect();
75
76    crate::issue::similarity::issue_text_similarity(issue_body, &other_issue_bodies)
77}
78
79/// Logs the job error logs to the info log in a readable summary
80pub fn log_info_downloaded_job_error_logs(job_error_logs: &[JobErrorLog]) {
81    log::info!("Got {} job error log(s)", job_error_logs.len());
82    for log in job_error_logs {
83        log::info!(
84            "\n\
85                        \tName: {name}\n\
86                        \tJob ID: {job_id}\
87                        {failed_steps}",
88            name = log.job_name,
89            job_id = log.job_id,
90            failed_steps = log
91                .failed_step_logs
92                .iter()
93                .fold(String::new(), |acc, step| {
94                    format!(
95                        "{acc}\n\t Step: {step_name} | Log length: {log_len}",
96                        acc = acc,
97                        step_name = step.step_name,
98                        log_len = step.contents().len()
99                    )
100                })
101        );
102    }
103}
104
105/// Extracts the error logs from the logs, failed jobs and failed steps
106/// and returns a vector of [JobErrorLog].
107///
108/// The extraction is performed by taking the name of each failed step in each failed job
109/// and searching for a log with a name that contains both the job name and the step name.
110///
111/// If a log is found, it is added to the [JobErrorLog] struct.
112///
113/// If a log is not found, an error is logged and the function continues.
114pub fn job_error_logs_from_log_and_failed_jobs_and_steps(
115    logs: &[JobLog],
116    failed_jobs: &[&Job],
117    failed_steps: &[&Step],
118) -> Vec<JobErrorLog> {
119    let mut job_error_logs: Vec<JobErrorLog> = Vec::new();
120    for job in failed_jobs {
121        log::info!("Extracting error logs for job: {}", job.name);
122        let name = job.name.clone();
123        let step_error_logs: Vec<StepErrorLog> =
124            find_error_logs_for_job_steps(logs, &name, failed_steps);
125        job_error_logs.push(JobErrorLog::new(job.id, name, step_error_logs));
126    }
127    job_error_logs
128}
129
130/// Finds the error logs for each step in the job and returns a vector of [StepErrorLog].
131fn find_error_logs_for_job_steps(
132    logs: &[JobLog],
133    job_name: &str,
134    steps: &[&Step],
135) -> Vec<StepErrorLog> {
136    steps
137        .iter()
138        .filter_map(|step| {
139            let step_name = step.name.clone();
140            let job_lob = match find_error_log(logs, job_name, &step_name) {
141                Some(log) => log,
142                None => {
143                    log::error!("No log found for failed step: {step_name} in job: {job_name}. Continuing...");
144                    return None;
145                }
146            };
147            Some(StepErrorLog::new(step_name, job_lob.content.clone()))
148        })
149        .collect()
150}
151
152/// Finds the error log in the logs that contains the job name and the step name.
153/// If no log is found, None is returned.
154fn find_error_log<'j>(logs: &'j [JobLog], job_name: &str, step_name: &str) -> Option<&'j JobLog> {
155    logs.iter()
156        .find(|log| log.name.contains(step_name) && log.name.contains(job_name))
157}