fix(#2752): pr-status must not abort on a repo with no CI
On a repo with no CI configured forgejo returns the combined-status `statuses` field as an explicit `null` rather than `[]`. `#[serde(default)]` only covers a *missing* key — a present null still fails to deserialize, so `pr-status` died with `invalid type: null, expected a sequence` instead of reporting the PR. Deserialize the field through an `Option<Vec<_>>` so both null and absent map to an empty vec. Closes #2752.
This commit is contained in:
parent
de09628c7c
commit
80650041d9
1 changed files with 26 additions and 1 deletions
|
|
@ -12,6 +12,7 @@
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use clap::Args as ClapArgs;
|
use clap::Args as ClapArgs;
|
||||||
use forgejo_api::structs::IssueGetCommentsQuery;
|
use forgejo_api::structs::IssueGetCommentsQuery;
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::client::{Client, index};
|
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
|
/// Keeping `state` a plain `String` (and the per-context statuses as
|
||||||
/// opaque `Value`s, which the render helpers already walk) means an
|
/// opaque `Value`s, which the render helpers already walk) means an
|
||||||
/// unknown or empty state is reported rather than fatal.
|
/// 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)]
|
#[derive(serde::Deserialize, Default)]
|
||||||
pub(crate) struct CombinedStatus {
|
pub(crate) struct CombinedStatus {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub state: String,
|
pub state: String,
|
||||||
#[serde(default)]
|
#[serde(default, deserialize_with = "null_as_empty")]
|
||||||
pub statuses: Vec<Value>,
|
pub statuses: Vec<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserialize a possibly-null JSON array as an empty `Vec`.
|
||||||
|
fn null_as_empty<'de, D>(de: D) -> Result<Vec<Value>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Ok(Option::<Vec<Value>>::deserialize(de)?.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
/// CI-only path for an explicit commit. Exit code mirrors the CI verdict.
|
/// CI-only path for an explicit commit. Exit code mirrors the CI verdict.
|
||||||
fn sha_status(client: &Client, sha: &str) -> Result<()> {
|
fn sha_status(client: &Client, sha: &str) -> Result<()> {
|
||||||
let (state, statuses) = fetch_combined(client, sha)?;
|
let (state, statuses) = fetch_combined(client, sha)?;
|
||||||
|
|
@ -413,6 +429,15 @@ mod tests {
|
||||||
assert!(c.statuses.is_empty());
|
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]
|
#[test]
|
||||||
fn combined_json_shape() {
|
fn combined_json_shape() {
|
||||||
let v = combined_json("abc", "success", &[]);
|
let v = combined_json("abc", "success", &[]);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue