diff --git a/swarm-controller/src/forge.rs b/swarm-controller/src/forge.rs index 52979830..3092c518 100644 --- a/swarm-controller/src/forge.rs +++ b/swarm-controller/src/forge.rs @@ -900,6 +900,9 @@ impl Client { /// /// A single repo's list failing does not fail the whole scan — logged and /// skipped, so one flaky repo can't blank out every other agent's status. + /// Repos the listing marks `empty` are skipped without a PR call: forgejo + /// answers 404 for the PR list of a repo with no branches, and such a repo + /// cannot carry a PR. pub async fn list_open_config_prs( &self, ) -> Result> { @@ -914,6 +917,9 @@ impl Client { let Some(agent) = repo.name.as_deref() else { continue; }; + if repo.empty.unwrap_or(false) { + continue; + } let query = RepoListPullRequestsQuery { state: Some(RepoListPullRequestsQueryState::Open), sort: None, @@ -2771,6 +2777,58 @@ mod tests { (Client { api, url }, seen) } + #[tokio::test] + async fn the_config_pr_scan_skips_empty_repos_without_a_pr_call() { + use axum::http::{StatusCode, Uri}; + + let seen = Arc::new(std::sync::Mutex::new(Vec::new())); + let recorded = Arc::clone(&seen); + let app = axum::Router::new().fallback(move |uri: Uri| { + recorded.lock().unwrap().push(uri.path().to_owned()); + let (status, count, body) = match uri.path() { + "/api/v1/orgs/agent-configs/repos" => ( + 200, + "2", + r#"[{"name":"bare","empty":true},{"name":"full","empty":false}]"#, + ), + "/api/v1/repos/agent-configs/full/pulls" => ( + 200, + "1", + r#"[{"number":4,"requested_reviewers":[],"html_url":"http://forge/agent-configs/full/pulls/4"}]"#, + ), + _ => (404, "0", r#"{"message":"not found"}"#), + }; + async move { + ( + StatusCode::from_u16(status).unwrap(), + [("x-total-count", count)], + body, + ) + } + }); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = url::Url::parse(&format!("http://{}/", listener.local_addr().unwrap())).unwrap(); + tokio::spawn(async move { axum::serve(listener, app).await }); + let api = crate::forge::api::tests::fixed(url.clone(), "stub-token"); + let client = Client { api, url }; + + let out = client.list_open_config_prs().await.unwrap(); + + assert_eq!(out.keys().collect::>(), ["full"]); + assert_eq!(out["full"].pr_number, 4); + let paths = seen.lock().unwrap(); + assert!( + paths + .iter() + .any(|p| p.ends_with("/repos/agent-configs/full/pulls")) + ); + assert!( + !paths + .iter() + .any(|p| p.contains("/repos/agent-configs/bare/")) + ); + } + /// One PR commit whose parent is `mb`. const ON_MB: &str = r#"[{"sha":"c1","parents":[{"sha":"mb"}]}]"#;