gh_workflow_parser/gh/
util.rs1use std::{error::Error, process::Command};
2
3use serde::{Deserialize, Serialize};
4
5use crate::gh::gh_cli;
6
7pub fn repo_url_to_job_url(repo_url: &str, run_id: &str, job_id: &str) -> String {
8 let run_url = repo_url_to_run_url(repo_url, run_id);
9 run_url_to_job_url(&run_url, job_id)
10}
11
12pub fn repo_url_to_run_url(repo_url: &str, run_id: &str) -> String {
13 format!("{repo_url}/actions/runs/{run_id}")
14}
15
16pub fn run_url_to_job_url(run_url: &str, job_id: &str) -> String {
17 format!("{run_url}/job/{job_id}")
18}
19
20pub fn run_summary(repo: &str, run_id: &str) -> Result<String, Box<dyn Error>> {
21 let output = Command::new(gh_cli())
22 .arg("run")
23 .arg(format!("--repo={repo}"))
24 .arg("view")
25 .arg(run_id)
26 .output()?;
27
28 assert!(
29 output.status.success(),
30 "Failed to get logs for repo={repo} run_id={run_id}. Failure: {stderr}",
31 stderr = String::from_utf8_lossy(&output.stderr)
32 );
33
34 Ok(String::from_utf8_lossy(&output.stdout).to_string())
35}
36
37pub fn failed_job_log(repo: &str, job_id: &str) -> Result<String, Box<dyn Error>> {
38 let output = Command::new(gh_cli())
39 .arg("run")
40 .arg("view")
41 .arg("--repo")
42 .arg(repo)
43 .arg("--job")
44 .arg(job_id)
45 .arg("--log-failed")
46 .output()?;
47
48 assert!(
49 output.status.success(),
50 "Failed to get logs for job ID: {job_id}. Failure: {stderr}",
51 stderr = String::from_utf8_lossy(&output.stderr)
52 );
53
54 Ok(String::from_utf8_lossy(&output.stdout).to_string())
55}
56
57pub fn create_issue(
59 repo: &str,
60 title: &str,
61 body: &str,
62 labels: &[String],
63) -> Result<(), Box<dyn Error>> {
64 let existing_labels = all_labels(repo)?;
66 for label in labels {
67 if !existing_labels.contains(label) {
68 log::info!("Label {label} does not exist in the repository. Creating it...");
69 create_label(repo, label, "FF0000", "", false)?;
70 } else {
71 log::debug!(
72 "Label {label} already exists in the repository, continuing without creating it."
73 )
74 }
75 }
76 let labels = labels.join(",");
78 let mut command = Command::new(gh_cli());
79 command
80 .arg("issue")
81 .arg("create")
82 .arg("--repo")
83 .arg(repo)
84 .arg("--title")
85 .arg(title)
86 .arg("--body")
87 .arg(body)
88 .arg("--label")
89 .arg(labels);
90
91 log::debug!("Debug view of command struct: {command:?}");
92 let output = command.output()?;
94
95 assert!(
96 output.status.success(),
97 "Failed to create issue. Failure: {stderr}",
98 stderr = String::from_utf8_lossy(&output.stderr)
99 );
100
101 Ok(())
102}
103
104pub fn issue_bodies_open_with_label(
106 repo: &str,
107 label: &str,
108) -> Result<Vec<String>, Box<dyn Error>> {
109 let output = Command::new(gh_cli())
110 .arg("issue")
111 .arg("list")
112 .arg("--repo")
113 .arg(repo)
114 .arg("--label")
115 .arg(label)
116 .arg("--json")
117 .arg("body")
118 .output()
119 .expect("Failed to list issues");
120
121 assert!(
122 output.status.success(),
123 "Failed to list issues. Failure: {stderr}",
124 stderr = String::from_utf8_lossy(&output.stderr)
125 );
126
127 let output = String::from_utf8_lossy(&output.stdout);
128
129 #[derive(Serialize, Deserialize)]
131 struct GhIssueBody {
132 pub body: String,
133 }
134
135 let parsed: Vec<GhIssueBody> = serde_json::from_str(&output)?;
136 Ok(parsed.into_iter().map(|item| item.body).collect())
137}
138
139pub fn all_labels(repo: &str) -> Result<Vec<String>, Box<dyn Error>> {
141 let output = Command::new(gh_cli())
142 .arg("--repo")
143 .arg(repo)
144 .arg("label")
145 .arg("list")
146 .arg("--json")
147 .arg("name")
148 .output()?;
149
150 assert!(
151 output.status.success(),
152 "Failed to list labels. Failure: {stderr}",
153 stderr = String::from_utf8_lossy(&output.stderr)
154 );
155
156 let output = String::from_utf8_lossy(&output.stdout);
158 #[derive(Serialize, Deserialize)]
159 struct Label {
160 name: String,
161 }
162 let parsed: Vec<Label> = serde_json::from_str(&output)?;
163 Ok(parsed.into_iter().map(|label| label.name).collect())
164}
165
166pub fn create_label(
170 repo: &str,
171 name: &str,
172 color: &str,
173 description: &str,
174 force: bool,
175) -> Result<(), Box<dyn Error>> {
176 let mut cmd = Command::new(gh_cli());
177 cmd.arg("label")
178 .arg("create")
179 .arg(name)
180 .arg("--repo")
181 .arg(repo)
182 .arg("--color")
183 .arg(color)
184 .arg("--description")
185 .arg(description);
186
187 if force {
188 cmd.arg("--force");
189 }
190
191 let output = cmd.output()?;
192 assert!(
193 output.status.success(),
194 "Failed to create label. Failure: {stderr}",
195 stderr = String::from_utf8_lossy(&output.stderr)
196 );
197
198 Ok(())
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use pretty_assertions::assert_eq;
205
206 #[test]
207 #[ignore = "This test requires a GitHub repository"]
208 fn test_issue_body_display() {
209 let issue_bodies = issue_bodies_open_with_label(
210 "https://github.com/luftkode/distro-template",
211 "CI scheduled build",
212 )
213 .unwrap();
214 for body in issue_bodies {
215 println!("{body}");
216 }
217 }
218
219 #[test]
220 fn test_parse_json_body() {
221 #[derive(Serialize, Deserialize)]
223 struct GhIssueBody {
224 pub body: String,
225 }
226
227 let data = r#"
228 [
229 {
230 "body": "**Run ID**: 7858139663 [LINK TO RUN](github.com/luftkode/distro-template/actions/runs/7858139663)\\n\\n**1 job failed:**\\n- **`Test template xilinx`**\\n\\n### `Test template xilinx` (ID 21442749267)\\n**Step failed:** `📦 Build yocto image`\\n\\\\n**Log:** github.com/luftkode/distro-template/actions/runs/7858139663/job/21442749267\\n\\\\n*Best effort error summary*:\\n``\\nERROR: sqlite3-native-3_3.43.2-r0 do_fetch: Bitbake Fetcher Error: MalformedUrl('${SOURCE_MIRROR_URL}')\\nERROR: Logfile of failure stored in: /app/yocto/build/tmp/work/x86_64-linux/sqlite3-native/3.43.2/temp/log.do_fetch.21616\\nERROR: Task (virtual:native:/app/yocto/build/../poky/meta/recipes-support/sqlite/sqlite3_3.43.2.bb:do_fetch) failed with exit code '1'\\n\\n2024-02-11 00:09:04 - ERROR - Command \"/app/yocto/poky/bitbake/bin/bitbake -c build test-template-ci-xilinx-image package-index\" failed with error 1\\n```"
231 },
232 {
233 "body": "Build failed on xilinx. Check the logs at https://github.com/luftkode/distro-template/actions/runs/7858139663 for more details."
234 },
235 {
236 "body": "Build failed on xilinx. Check the logs at https://github.com/luftkode/distro-template/actions/runs/7850874958 for more details."
237 }
238 ]
239 "#;
240
241 let parsed: Vec<GhIssueBody> = serde_json::from_str(data).unwrap();
243
244 let bodies: Vec<String> = parsed.into_iter().map(|item| item.body).collect();
246
247 assert_eq!(bodies.len(), 3);
249 assert!(bodies[0].contains("**Run ID**: 7858139663 [LINK TO RUN]("));
250 assert_eq!(
251 bodies[1],
252 "Build failed on xilinx. Check the logs at https://github.com/luftkode/distro-template/actions/runs/7858139663 for more details.");
253 assert_eq!(
254 bodies[2],
255 "Build failed on xilinx. Check the logs at https://github.com/luftkode/distro-template/actions/runs/7850874958 for more details.");
256 }
257}