Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/squabble-cli/src/zero_job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,9 @@ impl GhRuns {
let jobs_path = format!("repos/{slug}/actions/runs/{}/jobs?per_page=1", run.id);
if let Ok(jobs) = transport.get(&jobs_path) {
if (200..300).contains(&jobs.status) {
// Absence of a count is recorded as `None`, not as zero.
run.jobs = parse_job_count(&jobs.body).ok();
let count = parse_job_count(&jobs.body)
.map_err(|err| format!("invalid job evidence for `{jobs_path}`: {err}"))?;
run.jobs = Some(count);
}
}
}
Expand Down
15 changes: 12 additions & 3 deletions crates/squabble-fight/src/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,21 @@ pub fn parse_startup_runs(body: &str) -> Result<Vec<ZeroJobRun>, String> {
/// # Errors
///
/// Returns an error for invalid JSON or when `total_count` is absent or is not
/// an unsigned integer. Zero is a valid count.
/// an unsigned integer, or when a present `jobs` field is not an array or has
/// more entries than `total_count`. Zero is a valid count.
pub fn parse_job_count(body: &str) -> Result<u64, String> {
let v: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("could not parse actions/jobs response: {e}"))?;
v.get("total_count")
let count = v.get("total_count")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| "actions/jobs response has no `total_count`".to_string())
.ok_or_else(|| "actions/jobs response has no `total_count`".to_string())?;
if let Some(jobs) = v.get("jobs") {
let jobs = jobs.as_array().ok_or("actions/jobs response has invalid `jobs`")?;
if jobs.len() as u64 > count {
return Err("actions/jobs response lists more jobs than its total_count".to_string());
Comment on lines +240 to +241

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate contradictory job evidence instead of discarding it

When a response has total_count: 0 and a nonempty jobs array—the contradiction this check targets—the returned error is immediately converted to None by GhRuns::startup_runs via parse_job_count(&jobs.body).ok(). Runs with jobs: None still proceed through collect and are described as jobs=0, no check run exists, so an observed job-level failure can still receive zero-job attribution and incorrect remediation. Propagate this contradiction or exclude the run rather than degrading it to an unreadable count.

Useful? React with 👍 / 👎.

}
}
Ok(count)
}

#[cfg(test)]
Expand Down Expand Up @@ -510,6 +518,7 @@ jobs:
parse_job_count("{\"total_count\":0,\"jobs\":[]}").unwrap(),
0
);
assert!(parse_job_count(r#"{"total_count":0,"jobs":[{"id":1}]}"#).is_err());
assert!(parse_job_count("{}").is_err());
assert!(parse_job_count("[]").is_err());
}
Expand Down
Loading