oven_cli/issues/
github.rs1use 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
12pub 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}