diff --git a/src/github/actions.rs b/src/github/actions.rs index bc3b4822..22055727 100644 --- a/src/github/actions.rs +++ b/src/github/actions.rs @@ -4,12 +4,12 @@ use crate::github::models::WorkflowRun; use crate::github::{Client, GitHubError, RepoId}; -fn parse_runs(envelope: serde_json::Value) -> Result, GitHubError> { - let arr = envelope - .get("workflow_runs") +/// Extractor for the `{ workflow_runs: [...] }` envelope each page returns. +fn workflow_runs(page: &serde_json::Value) -> Vec { + page.get("workflow_runs") + .and_then(|v| v.as_array()) .cloned() - .unwrap_or(serde_json::Value::Array(vec![])); - serde_json::from_value(arr).map_err(|e| GitHubError::Parse(e.to_string())) + .unwrap_or_default() } fn dispatch_body(git_ref: &str, inputs: Option<&serde_json::Value>) -> serde_json::Value { @@ -29,8 +29,8 @@ pub fn list_runs( if let Some(b) = branch { path.push_str(&format!("?branch={}", crate::github::encode_query(b))); } - let envelope = client.request("GET", &path, None)?; - parse_runs(envelope) + let arr = client.get_paginated(&path, workflow_runs)?; + serde_json::from_value(arr).map_err(|e| GitHubError::Parse(e.to_string())) } pub fn get_run(client: &Client, repo: &RepoId, run_id: u64) -> Result { @@ -68,17 +68,17 @@ pub fn dispatch( mod tests { use super::*; #[test] - fn parse_runs_extracts_the_array() { + fn workflow_runs_extracts_the_array() { let env = serde_json::json!({ "total_count": 1, "workflow_runs": [{ "id": 1, "status": "completed", "conclusion": "success", "html_url": "https://github.com/o/r/actions/runs/1" }] }); - let runs = parse_runs(env).unwrap(); - assert_eq!(runs.len(), 1); - assert_eq!(runs[0].conclusion.as_deref(), Some("success")); + let items = workflow_runs(&env); + assert_eq!(items.len(), 1); + assert_eq!(items[0]["conclusion"], "success"); } #[test] - fn parse_runs_defaults_to_empty_when_key_absent() { - assert!(parse_runs(serde_json::json!({})).unwrap().is_empty()); + fn workflow_runs_defaults_to_empty_when_key_absent() { + assert!(workflow_runs(&serde_json::json!({})).is_empty()); } #[test] fn dispatch_body_includes_inputs_only_when_present() { diff --git a/src/github/client.rs b/src/github/client.rs index 3cd7930a..8eab2112 100644 --- a/src/github/client.rs +++ b/src/github/client.rs @@ -13,6 +13,9 @@ pub struct Client { const BASE_URL: &str = "https://api.github.com"; +/// Max items per page GitHub allows for the list endpoints used here. +const PER_PAGE: usize = 100; + fn map_status(status: u16, ratelimit_remaining: Option<&str>, body: String) -> GitHubError { match status { 401 => GitHubError::NoAuth( @@ -97,6 +100,38 @@ impl Client { Err(e) => Err(GitHubError::Transport(e.to_string())), } } + + /// GET every page of a list endpoint, walking `?page=N&per_page=100` until a + /// page comes back shorter than a full page. `extract` pulls the item array + /// out of each response — identity for bare-array endpoints, or the envelope + /// key (e.g. `workflow_runs`) for wrapped ones. Returns the concatenated + /// items as a JSON array so callers deserialize once. + pub fn get_paginated( + &self, + base_path: &str, + extract: impl Fn(&serde_json::Value) -> Vec, + ) -> Result { + let sep = if base_path.contains('?') { '&' } else { '?' }; + let mut all: Vec = Vec::new(); + let mut page = 1usize; + loop { + let path = format!("{base_path}{sep}per_page={PER_PAGE}&page={page}"); + let resp = self.request("GET", &path, None)?; + let items = extract(&resp); + let full = items.len() == PER_PAGE; + all.extend(items); + if !full { + break; + } + page += 1; + } + Ok(serde_json::Value::Array(all)) + } +} + +/// Extractor for bare-array list endpoints (pulls, issues, releases). +pub fn as_array(v: &serde_json::Value) -> Vec { + v.as_array().cloned().unwrap_or_default() } #[cfg(test)] @@ -126,4 +161,11 @@ mod tests { GitHubError::Http { status: 500, .. } )); } + + #[test] + fn as_array_unwraps_or_defaults_empty() { + assert_eq!(as_array(&serde_json::json!([1, 2, 3])).len(), 3); + assert!(as_array(&serde_json::json!({})).is_empty()); + assert!(as_array(&serde_json::Value::Null).is_empty()); + } } diff --git a/src/github/issues.rs b/src/github/issues.rs index 9b2c13e6..e263decf 100644 --- a/src/github/issues.rs +++ b/src/github/issues.rs @@ -50,7 +50,7 @@ pub fn list(client: &Client, repo: &RepoId, state: &str) -> Result, G repo.repo, crate::github::encode_query(state) ); - let json = client.request("GET", &path, None)?; + let json = client.get_paginated(&path, crate::github::client::as_array)?; serde_json::from_value(json).map_err(|e| GitHubError::Parse(e.to_string())) } diff --git a/src/github/pulls.rs b/src/github/pulls.rs index ef0f220e..db07b4f8 100644 --- a/src/github/pulls.rs +++ b/src/github/pulls.rs @@ -29,7 +29,7 @@ pub fn list(client: &Client, repo: &RepoId, state: &str) -> Result Result, GitHubError> { let path = format!("/repos/{}/{}/releases", repo.owner, repo.repo); - let json = client.request("GET", &path, None)?; + let json = client.get_paginated(&path, crate::github::client::as_array)?; serde_json::from_value(json).map_err(|e| GitHubError::Parse(e.to_string())) }