diff --git a/hive-forge/src/verbs/pr_status.rs b/hive-forge/src/verbs/pr_status.rs index dc1f35d8..11746fa6 100644 --- a/hive-forge/src/verbs/pr_status.rs +++ b/hive-forge/src/verbs/pr_status.rs @@ -12,6 +12,7 @@ use anyhow::{Context, Result, bail}; use clap::Args as ClapArgs; use forgejo_api::structs::IssueGetCommentsQuery; +use serde::Deserialize; use serde_json::Value; use crate::client::{Client, index}; @@ -53,14 +54,29 @@ pub fn run(client: &Client, args: Args) -> Result<()> { /// Keeping `state` a plain `String` (and the per-context statuses as /// opaque `Value`s, which the render helpers already walk) means an /// unknown or empty state is reported rather than fatal. +/// +/// `statuses` needs the same leniency for a different reason: on a repo +/// with no CI at all the field comes back as an explicit `null` rather +/// than `[]`, and `#[serde(default)]` only covers a *missing* key — a +/// present null still fails to deserialize. So a doc-only repo makes the +/// whole verb error out on exactly the PRs where "no CI here" is the +/// answer worth printing. #[derive(serde::Deserialize, Default)] pub(crate) struct CombinedStatus { #[serde(default)] pub state: String, - #[serde(default)] + #[serde(default, deserialize_with = "null_as_empty")] pub statuses: Vec, } +/// Deserialize a possibly-null JSON array as an empty `Vec`. +fn null_as_empty<'de, D>(de: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Ok(Option::>::deserialize(de)?.unwrap_or_default()) +} + /// CI-only path for an explicit commit. Exit code mirrors the CI verdict. fn sha_status(client: &Client, sha: &str) -> Result<()> { let (state, statuses) = fetch_combined(client, sha)?; @@ -413,6 +429,15 @@ mod tests { assert!(c.statuses.is_empty()); } + #[test] + fn combined_status_tolerates_null_statuses() { + // A repo with no CI configured: the field is present and null, + // which `#[serde(default)]` alone does not cover. + let c: CombinedStatus = + serde_json::from_str(r#"{"state":"","statuses":null,"sha":"abc"}"#).unwrap(); + assert!(c.statuses.is_empty()); + } + #[test] fn combined_json_shape() { let v = combined_json("abc", "success", &[]);