Skip to main content

oven_cli/issues/
github.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use async_trait::async_trait;
5
6use super::{IssueOrigin, IssueProvider, PipelineIssue};
7use crate::{
8    github::{self, GhClient, issues::parse_issue_frontmatter},
9    process::CommandRunner,
10};
11
12/// Wraps `GhClient` to implement `IssueProvider` for GitHub issues.
13pub struct GithubIssueProvider<R: CommandRunner> {
14    client: Arc<GhClient<R>>,
15    target_field: String,
16}
17
18impl<R: CommandRunner> GithubIssueProvider<R> {
19    pub fn new(client: Arc<GhClient<R>>, target_field: &str) -> Self {
20        Self { client, target_field: target_field.to_string() }
21    }
22}
23
24#[async_trait]
25impl<R: CommandRunner + 'static> IssueProvider for GithubIssueProvider<R> {
26    async fn get_ready_issues(&self, label: &str) -> Result<Vec<PipelineIssue>> {
27        let issues = self.client.get_issues_by_label(label).await?;
28        Ok(issues
29            .into_iter()
30            .map(|i| {
31                let parsed = parse_issue_frontmatter(&i, &self.target_field);
32                PipelineIssue {
33                    number: i.number,
34                    title: i.title,
35                    body: parsed.body_without_frontmatter,
36                    source: IssueOrigin::Github,
37                    target_repo: parsed.target_repo,
38                }
39            })
40            .collect())
41    }
42
43    async fn get_issue(&self, number: u32) -> Result<PipelineIssue> {
44        let issue = self.client.get_issue(number).await?;
45        let parsed = parse_issue_frontmatter(&issue, &self.target_field);
46        Ok(PipelineIssue {
47            number: issue.number,
48            title: issue.title,
49            body: parsed.body_without_frontmatter,
50            source: IssueOrigin::Github,
51            target_repo: parsed.target_repo,
52        })
53    }
54
55    async fn transition(&self, number: u32, from: &str, to: &str) -> Result<()> {
56        github::transition_issue(&self.client, number, from, to).await
57    }
58
59    async fn comment(&self, number: u32, body: &str) -> Result<()> {
60        self.client.comment_on_issue(number, body).await
61    }
62
63    async fn close(&self, number: u32, comment: Option<&str>) -> Result<()> {
64        self.client.close_issue(number, comment).await
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use std::path::Path;
71
72    use super::*;
73    use crate::process::{CommandOutput, MockCommandRunner};
74
75    #[tokio::test]
76    async fn get_ready_issues_maps_to_pipeline_issues() {
77        let mut mock = MockCommandRunner::new();
78        mock.expect_run_gh().returning(|_, _| {
79            Box::pin(async {
80                Ok(CommandOutput {
81                    stdout: r#"[{"number":1,"title":"Fix bug","body":"details","labels":[]}]"#
82                        .to_string(),
83                    stderr: String::new(),
84                    success: true,
85                })
86            })
87        });
88
89        let client = Arc::new(GhClient::new(mock, Path::new("/tmp")));
90        let provider = GithubIssueProvider::new(client, "target_repo");
91        let issues = provider.get_ready_issues("o-ready").await.unwrap();
92
93        assert_eq!(issues.len(), 1);
94        assert_eq!(issues[0].number, 1);
95        assert_eq!(issues[0].source, IssueOrigin::Github);
96        assert!(issues[0].target_repo.is_none());
97    }
98
99    #[tokio::test]
100    async fn get_ready_issues_extracts_target_repo() {
101        let mut mock = MockCommandRunner::new();
102        mock.expect_run_gh().returning(|_, _| {
103            Box::pin(async {
104                Ok(CommandOutput {
105                    stdout: r#"[{"number":2,"title":"Multi","body":"---\ntarget_repo: api\n---\n\nBody","labels":[]}]"#
106                        .to_string(),
107                    stderr: String::new(),
108                    success: true,
109                })
110            })
111        });
112
113        let client = Arc::new(GhClient::new(mock, Path::new("/tmp")));
114        let provider = GithubIssueProvider::new(client, "target_repo");
115        let issues = provider.get_ready_issues("o-ready").await.unwrap();
116
117        assert_eq!(issues[0].target_repo.as_deref(), Some("api"));
118        assert_eq!(issues[0].body, "Body");
119    }
120
121    #[tokio::test]
122    async fn transition_delegates_to_gh_client() {
123        let mut mock = MockCommandRunner::new();
124        mock.expect_run_gh().returning(|_, _| {
125            Box::pin(async {
126                Ok(CommandOutput { stdout: String::new(), stderr: String::new(), success: true })
127            })
128        });
129
130        let client = Arc::new(GhClient::new(mock, Path::new("/tmp")));
131        let provider = GithubIssueProvider::new(client, "target_repo");
132        let result = provider.transition(1, "o-ready", "o-cooking").await;
133        assert!(result.is_ok());
134    }
135}