use anyhow::Result;
use crate::github::{self, ActionsJob, Check, CheckState};
use crate::issue::IssueUrl;
use crate::poll;
use crate::progress;
pub enum Ci {
Absent,
Passed,
Failed(FailedChecks),
}
impl Ci {
pub fn has_own_failures(&self) -> bool {
matches!(self, Ci::Failed(failed) if !failed.own.is_empty())
}
}
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);
}
watch_to_end(issue, sha, base_commit)
}
pub fn rerun(
issue: &IssueUrl,
sha: &str,
base_commit: Option<&str>,
failed: &FailedChecks,
) -> Result<Option<Ci>> {
let short = short(sha);
let not_jobs = failed.own.iter().filter(|check| check.job.is_none());
let not_jobs = check_names(not_jobs);
if !not_jobs.is_empty() {
progress::step(format_args!(
"the failed checks on {short} can't be re-run: not GitHub Actions jobs: {not_jobs}"
));
return Ok(None);
}
progress::step(format_args!(
"re-running the failed checks on {short}: {}",
check_names(&failed.own)
));
let mut workflow_runs: Vec<u64> = jobs_of(&failed.own).map(|job| job.workflow_run).collect();
workflow_runs.sort_unstable();
workflow_runs.dedup();
for workflow_run in &workflow_runs {
if let Err(error) = github::rerun_failed_jobs(issue, *workflow_run) {
progress::step(format_args!(
"the failed checks on {short} were not re-run: {error:#}"
));
return Ok(None);
}
}
let previous_attempt: Vec<u64> = jobs_of(&failed.own)
.chain(jobs_of(&failed.inherited))
.filter(|job| workflow_runs.contains(&job.workflow_run))
.map(|job| job.check_run)
.collect();
let grace = poll::grace_period();
let new_attempt = poll::within(grace, || {
let checks = github::checks_on(issue, sha)?;
let still_listed = jobs_of(&checks).any(|job| previous_attempt.contains(&job.check_run));
Ok((!still_listed).then_some(()))
})?;
if new_attempt.is_none() {
progress::step(format_args!(
"no re-run appeared on {short} within {}s",
grace.as_secs()
));
return Ok(None);
}
watch_to_end(issue, sha, base_commit).map(Some)
}
fn watch_to_end(issue: &IssueUrl, sha: &str, base_commit: Option<&str>) -> Result<Ci> {
let short = short(sha);
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<'a>(checks: impl IntoIterator<Item = &'a Check>) -> String {
let names: Vec<&str> = checks
.into_iter()
.map(|check| check.name.as_str())
.collect();
names.join(", ")
}
fn jobs_of(checks: &[Check]) -> impl Iterator<Item = ActionsJob> + '_ {
checks.iter().filter_map(|check| check.job)
}
pub fn check_list(checks: &[Check]) -> String {
checks
.iter()
.map(|check| format!("- {}\n", check_with_url(check)))
.collect()
}
pub fn check_with_url(check: &Check) -> String {
match &check.url {
Some(url) => format!("{}: {url}", check.name),
None => check.name.clone(),
}
}
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()
}