Skip to content

Commit 9d46802

Browse files
committed
feat: wire GitHub API client and task status endpoint
1 parent 7c1449f commit 9d46802

11 files changed

Lines changed: 956 additions & 71 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ anyhow = "1"
2020
async-trait = "0.1"
2121
axum = { version = "0.7", features = ["tokio"] }
2222
clap = { version = "4", features = ["derive"] }
23+
chrono = "0.4"
2324
hmac = "0.12"
2425
jsonwebtoken = "9"
2526
octocrab = "0.41"

crates/github/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ license.workspace = true
77
[dependencies]
88
anyhow.workspace = true
99
async-trait.workspace = true
10+
chrono.workspace = true
1011
jsonwebtoken.workspace = true
1112
octocrab.workspace = true
1213
reqwest.workspace = true

crates/github/src/check_run.rs

Lines changed: 278 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! GitHub Check Runs API client.
22
33
use anyhow::Result;
4+
use serde::Deserialize;
5+
6+
use crate::{client, send_json, GitHubRequest, DEFAULT_API_BASE_URL};
47

58
/// Check Run status values.
69
#[derive(Debug, Clone)]
@@ -20,6 +23,11 @@ pub enum CheckConclusion {
2023
ActionRequired,
2124
}
2225

26+
#[derive(Debug, Deserialize)]
27+
struct CheckRunResponse {
28+
id: u64,
29+
}
30+
2331
/// Creates a new Check Run and returns its ID.
2432
pub async fn create(
2533
installation_token: &str,
@@ -29,14 +37,42 @@ pub async fn create(
2937
name: &str,
3038
details_url: Option<&str>,
3139
) -> Result<u64> {
32-
// TODO: implement via reqwest + GitHub REST API
33-
// POST /repos/{owner}/{repo}/check-runs
3440
tracing::info!(
3541
repo = %format!("{repo_owner}/{repo_name}"),
3642
name,
3743
"creating check run"
3844
);
39-
Ok(0) // placeholder
45+
create_with_base_url(
46+
DEFAULT_API_BASE_URL,
47+
installation_token,
48+
repo_owner,
49+
repo_name,
50+
head_sha,
51+
name,
52+
details_url,
53+
)
54+
.await
55+
}
56+
57+
pub async fn create_with_base_url(
58+
api_base_url: &str,
59+
installation_token: &str,
60+
repo_owner: &str,
61+
repo_name: &str,
62+
head_sha: &str,
63+
name: &str,
64+
details_url: Option<&str>,
65+
) -> Result<u64> {
66+
let client = client()?;
67+
let response = send_json(
68+
&client,
69+
api_base_url,
70+
installation_token,
71+
create_request(repo_owner, repo_name, head_sha, name, details_url),
72+
)
73+
.await?;
74+
let body: CheckRunResponse = response.json().await?;
75+
Ok(body.id)
4076
}
4177

4278
/// Updates an existing Check Run with progress output.
@@ -50,7 +86,38 @@ pub async fn update(
5086
summary: &str,
5187
) -> Result<()> {
5288
tracing::info!(check_run_id, title, "updating check run");
53-
Ok(()) // placeholder
89+
update_with_base_url(
90+
DEFAULT_API_BASE_URL,
91+
installation_token,
92+
repo_owner,
93+
repo_name,
94+
check_run_id,
95+
status,
96+
title,
97+
summary,
98+
)
99+
.await
100+
}
101+
102+
pub async fn update_with_base_url(
103+
api_base_url: &str,
104+
installation_token: &str,
105+
repo_owner: &str,
106+
repo_name: &str,
107+
check_run_id: u64,
108+
status: CheckStatus,
109+
title: &str,
110+
summary: &str,
111+
) -> Result<()> {
112+
let client = client()?;
113+
send_json(
114+
&client,
115+
api_base_url,
116+
installation_token,
117+
update_request(repo_owner, repo_name, check_run_id, status, title, summary),
118+
)
119+
.await?;
120+
Ok(())
54121
}
55122

56123
/// Completes a Check Run.
@@ -64,5 +131,211 @@ pub async fn complete(
64131
summary: &str,
65132
) -> Result<()> {
66133
tracing::info!(check_run_id, title, "completing check run");
67-
Ok(()) // placeholder
134+
complete_with_base_url(
135+
DEFAULT_API_BASE_URL,
136+
installation_token,
137+
repo_owner,
138+
repo_name,
139+
check_run_id,
140+
conclusion,
141+
title,
142+
summary,
143+
)
144+
.await
145+
}
146+
147+
pub async fn complete_with_base_url(
148+
api_base_url: &str,
149+
installation_token: &str,
150+
repo_owner: &str,
151+
repo_name: &str,
152+
check_run_id: u64,
153+
conclusion: CheckConclusion,
154+
title: &str,
155+
summary: &str,
156+
) -> Result<()> {
157+
let client = client()?;
158+
send_json(
159+
&client,
160+
api_base_url,
161+
installation_token,
162+
complete_request(
163+
repo_owner,
164+
repo_name,
165+
check_run_id,
166+
conclusion,
167+
title,
168+
summary,
169+
),
170+
)
171+
.await?;
172+
Ok(())
173+
}
174+
175+
fn create_request(
176+
repo_owner: &str,
177+
repo_name: &str,
178+
head_sha: &str,
179+
name: &str,
180+
details_url: Option<&str>,
181+
) -> GitHubRequest {
182+
let mut body = serde_json::json!({
183+
"name": name,
184+
"head_sha": head_sha,
185+
"status": "queued",
186+
});
187+
if let Some(details_url) = details_url {
188+
body["details_url"] = serde_json::json!(details_url);
189+
}
190+
191+
GitHubRequest {
192+
method: "POST",
193+
path: format!("/repos/{repo_owner}/{repo_name}/check-runs"),
194+
body,
195+
}
196+
}
197+
198+
fn update_request(
199+
repo_owner: &str,
200+
repo_name: &str,
201+
check_run_id: u64,
202+
status: CheckStatus,
203+
title: &str,
204+
summary: &str,
205+
) -> GitHubRequest {
206+
GitHubRequest {
207+
method: "PATCH",
208+
path: format!("/repos/{repo_owner}/{repo_name}/check-runs/{check_run_id}"),
209+
body: serde_json::json!({
210+
"status": status.as_str(),
211+
"output": {
212+
"title": title,
213+
"summary": summary,
214+
}
215+
}),
216+
}
217+
}
218+
219+
fn complete_request(
220+
repo_owner: &str,
221+
repo_name: &str,
222+
check_run_id: u64,
223+
conclusion: CheckConclusion,
224+
title: &str,
225+
summary: &str,
226+
) -> GitHubRequest {
227+
GitHubRequest {
228+
method: "PATCH",
229+
path: format!("/repos/{repo_owner}/{repo_name}/check-runs/{check_run_id}"),
230+
body: serde_json::json!({
231+
"status": "completed",
232+
"conclusion": conclusion.as_str(),
233+
"output": {
234+
"title": title,
235+
"summary": summary,
236+
}
237+
}),
238+
}
239+
}
240+
241+
impl CheckStatus {
242+
fn as_str(&self) -> &'static str {
243+
match self {
244+
Self::Queued => "queued",
245+
Self::InProgress => "in_progress",
246+
Self::Completed => "completed",
247+
}
248+
}
249+
}
250+
251+
impl CheckConclusion {
252+
fn as_str(&self) -> &'static str {
253+
match self {
254+
Self::Success => "success",
255+
Self::Failure => "failure",
256+
Self::Neutral => "neutral",
257+
Self::Cancelled => "cancelled",
258+
Self::ActionRequired => "action_required",
259+
}
260+
}
261+
}
262+
263+
#[cfg(test)]
264+
mod tests {
265+
use super::*;
266+
use serde_json::json;
267+
268+
#[test]
269+
fn create_request_targets_check_runs_with_expected_payload() {
270+
let request = create_request(
271+
"octo",
272+
"repo",
273+
"abc123",
274+
"Cody — Fix issue #7",
275+
Some("https://cave.example/sessions/task-7"),
276+
);
277+
278+
assert_eq!(request.method, "POST");
279+
assert_eq!(request.path, "/repos/octo/repo/check-runs");
280+
assert_eq!(
281+
request.body,
282+
json!({
283+
"name": "Cody — Fix issue #7",
284+
"head_sha": "abc123",
285+
"status": "queued",
286+
"details_url": "https://cave.example/sessions/task-7"
287+
})
288+
);
289+
}
290+
291+
#[test]
292+
fn update_request_sets_output_without_conclusion() {
293+
let request = update_request(
294+
"octo",
295+
"repo",
296+
42,
297+
CheckStatus::InProgress,
298+
"Running",
299+
"Working",
300+
);
301+
302+
assert_eq!(request.method, "PATCH");
303+
assert_eq!(request.path, "/repos/octo/repo/check-runs/42");
304+
assert_eq!(
305+
request.body,
306+
json!({
307+
"status": "in_progress",
308+
"output": {
309+
"title": "Running",
310+
"summary": "Working"
311+
}
312+
})
313+
);
314+
}
315+
316+
#[test]
317+
fn complete_request_sets_completed_status_and_conclusion() {
318+
let request = complete_request(
319+
"octo",
320+
"repo",
321+
42,
322+
CheckConclusion::Success,
323+
"Done",
324+
"Ready",
325+
);
326+
327+
assert_eq!(request.method, "PATCH");
328+
assert_eq!(request.path, "/repos/octo/repo/check-runs/42");
329+
assert_eq!(
330+
request.body,
331+
json!({
332+
"status": "completed",
333+
"conclusion": "success",
334+
"output": {
335+
"title": "Done",
336+
"summary": "Ready"
337+
}
338+
})
339+
);
340+
}
68341
}

0 commit comments

Comments
 (0)