Skip to main content

oven_cli/github/
labels.rs

1use anyhow::{Context, Result};
2
3use super::GhClient;
4use crate::process::CommandRunner;
5
6/// Label colors for oven labels.
7const 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    /// Add a label to an issue.
16    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    /// Remove a label from an issue.
30    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        // Removing a label that doesn't exist is not an error
40        if !output.success && !output.stderr.contains("not found") {
41            anyhow::bail!("remove label failed: {}", output.stderr.trim());
42        }
43        Ok(())
44    }
45
46    /// Remove one label and add another in a single gh CLI call.
47    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    /// Ensure all oven labels exist in the repository.
62    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}