use anyhow::Result;
use crate::github::{self, Check, CheckState};
use crate::issue::IssueUrl;
use crate::poll;
use crate::progress;
pub enum Ci {
Absent,
Passed,
Failed(FailedChecks),
}
pub struct FailedChecks {
pub own: Vec<Check>,
pub inherited: Vec<Check>,
}
pub fn watch(issue: &IssueUrl, sha: &str, base_commit: Option<&str>) -> Result<Ci> {
let grace = poll::grace_period();
let short = short(sha);
progress::step(format_args!(
"waiting up to {}s for CI on {short}",
grace.as_secs()
));
let appeared = poll::within(grace, || {
Ok((!github::checks_on(issue, sha)?.is_empty()).then_some(()))
})?;
if appeared.is_none() {
progress::step(format_args!(
"no CI checks appeared on {short}; counting that as passing"
));
return Ok(Ci::Absent);
}
let mut reported_pending = None;
let checks = poll::until(|| {
let checks = github::checks_on(issue, sha)?;
let pending = count(&checks, CheckState::Pending);
if pending == 0 {
return Ok(Some(checks));
}
if reported_pending != Some(pending) {
progress::step(format_args!(
"CI on {short}: {pending} of {} checks still running",
checks.len()
));
reported_pending = Some(pending);
}
Ok(None)
})?;
let failed: Vec<Check> = checks.into_iter().filter(is_failed).collect();
if failed.is_empty() {
progress::step(format_args!("CI passed on {short}"));
return Ok(Ci::Passed);
}
progress::step(format_args!(
"CI failed on {short}: {}",
check_names(&failed)
));
let Some(base_commit) = base_commit else {
return Ok(Ci::Failed(FailedChecks {
own: failed,
inherited: Vec::new(),
}));
};
let on_base = github::checks_on(issue, base_commit)?;
let (inherited, own) = failed.into_iter().partition(|check| {
let mut same_name = on_base.iter().filter(|base| base.name == check.name);
same_name.next().is_some_and(is_failed) && same_name.all(is_failed)
});
Ok(Ci::Failed(FailedChecks { own, inherited }))
}
pub fn check_names(checks: &[Check]) -> String {
let names: Vec<&str> = checks.iter().map(|check| check.name.as_str()).collect();
names.join(", ")
}
pub fn check_list(checks: &[Check]) -> String {
checks
.iter()
.map(|check| match &check.url {
Some(url) => format!("- {}: {url}\n", check.name),
None => format!("- {}\n", check.name),
})
.collect()
}
pub fn short(sha: &str) -> &str {
&sha[..sha.len().min(7)]
}
fn is_failed(check: &Check) -> bool {
check.state == CheckState::Failed
}
fn count(checks: &[Check], state: CheckState) -> usize {
checks.iter().filter(|check| check.state == state).count()
}