oven_cli/github/
labels.rs1use anyhow::{Context, Result};
2
3use super::GhClient;
4use crate::process::CommandRunner;
5
6const LABEL_COLORS: &[(&str, &str, &str)] = &[
8 ("o-ready", "0E8A16", "Ready for oven pipeline pickup"),
9 ("o-cooking", "FBCA04", "Oven pipeline is working on this"),
10 ("o-complete", "1D76DB", "Oven pipeline completed successfully"),
11 ("o-failed", "D93F0B", "Oven pipeline failed"),
12];
13
14impl<R: CommandRunner> GhClient<R> {
15 pub async fn add_label(&self, issue_number: u32, label: &str) -> Result<()> {
17 let output = self
18 .runner
19 .run_gh(
20 &Self::s(&["issue", "edit", &issue_number.to_string(), "--add-label", label]),
21 &self.repo_dir,
22 )
23 .await
24 .context("adding label")?;
25 Self::check_output(&output, "add label")?;
26 Ok(())
27 }
28
29 pub async fn remove_label(&self, issue_number: u32, label: &str) -> Result<()> {
31 let output = self
32 .runner
33 .run_gh(
34 &Self::s(&["issue", "edit", &issue_number.to_string(), "--remove-label", label]),
35 &self.repo_dir,
36 )
37 .await
38 .context("removing label")?;
39 if !output.success && !output.stderr.contains("not found") {
41 anyhow::bail!("remove label failed: {}", output.stderr.trim());
42 }
43 Ok(())
44 }
45
46 pub async fn swap_labels(&self, issue_number: u32, remove: &str, add: &str) -> Result<()> {
48 let num = issue_number.to_string();
49 let output = self
50 .runner
51 .run_gh(
52 &Self::s(&["issue", "edit", &num, "--remove-label", remove, "--add-label", add]),
53 &self.repo_dir,
54 )
55 .await
56 .context("swapping labels")?;
57 Self::check_output(&output, "swap labels")?;
58 Ok(())
59 }
60
61 pub async fn ensure_labels_exist(&self) -> Result<()> {
63 for (name, color, description) in LABEL_COLORS {
64 let output = self
65 .runner
66 .run_gh(
67 &Self::s(&[
68 "label",
69 "create",
70 name,
71 "--color",
72 color,
73 "--description",
74 description,
75 "--force",
76 ]),
77 &self.repo_dir,
78 )
79 .await
80 .context("creating label")?;
81 Self::check_output(&output, &format!("create label {name}"))?;
82 }
83 Ok(())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use std::path::Path;
90
91 use crate::{github::GhClient, process::CommandOutput};
92
93 fn mock_runner(success: bool) -> crate::process::MockCommandRunner {
94 let mut mock = crate::process::MockCommandRunner::new();
95 mock.expect_run_gh().returning(move |_, _| {
96 Box::pin(async move {
97 Ok(CommandOutput { stdout: String::new(), stderr: String::new(), success })
98 })
99 });
100 mock
101 }
102
103 #[tokio::test]
104 async fn add_label_succeeds() {
105 let client = GhClient::new(mock_runner(true), Path::new("/tmp"));
106 let result = client.add_label(42, "o-cooking").await;
107 assert!(result.is_ok());
108 }
109
110 #[tokio::test]
111 async fn add_label_failure_propagates() {
112 let mut mock = crate::process::MockCommandRunner::new();
113 mock.expect_run_gh().returning(|_, _| {
114 Box::pin(async {
115 Ok(CommandOutput {
116 stdout: String::new(),
117 stderr: "not authorized".to_string(),
118 success: false,
119 })
120 })
121 });
122 let client = GhClient::new(mock, Path::new("/tmp"));
123 let result = client.add_label(42, "o-cooking").await;
124 assert!(result.is_err());
125 }
126
127 #[tokio::test]
128 async fn ensure_labels_exist_succeeds() {
129 let client = GhClient::new(mock_runner(true), Path::new("/tmp"));
130 let result = client.ensure_labels_exist().await;
131 assert!(result.is_ok());
132 }
133}