use std::time::Instant;
use chrono::Utc;
use tracing::{info, warn};
use super::auth_backoff::{AuthBackoff, is_auth_failure};
use super::driver::{ClaimEnder, Gh, Git, Landing};
use super::registry::{CleanupRegistry, CleanupScope, OpenedPr};
use super::{CallerOwnership, ClaimOwnership, CleanupRequest, DirtProbe, plan};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SweepDecision {
Clean,
AlreadyCleaned,
NotMerged(String),
}
pub fn sweep_decision(entry: &OpenedPr, view: &plan::PrView) -> SweepDecision {
if !entry.pending() {
return SweepDecision::AlreadyCleaned;
}
match plan::merge_refusal(view, entry.pr) {
Some(reason) => SweepDecision::NotMerged(reason),
None => SweepDecision::Clean,
}
}
pub async fn run_sweep<G: Gh, T: Git, C: ClaimEnder>(
gh: &G,
git: &T,
claims: &C,
landing: &dyn Landing,
probe_dirt: DirtProbe<'_>,
registry: &CleanupRegistry,
backoff: &AuthBackoff,
) -> usize {
let ownership = CallerOwnership::from_env();
run_sweep_with(
gh, git, claims, &ownership, landing, probe_dirt, registry, backoff,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_sweep_with<G: Gh, T: Git, C: ClaimEnder>(
gh: &G,
git: &T,
claims: &C,
ownership: &dyn ClaimOwnership,
landing: &dyn Landing,
probe_dirt: DirtProbe<'_>,
registry: &CleanupRegistry,
backoff: &AuthBackoff,
) -> usize {
let mut cleaned = 0usize;
for entry in registry.pending() {
if !backoff.may_poll(Instant::now()) {
break;
}
let req = CleanupRequest {
pr: entry.pr,
repo: Some(entry.repo.clone()),
repo_root: entry.repo_root.clone(),
dry_run: false,
head_only: entry.scope == CleanupScope::HeadOnly,
};
let view = match super::view_pr(gh, &req) {
Ok(v) => {
backoff.record_answer();
v
}
Err(e) => {
let detail = format!("{e:#}");
if is_auth_failure(&detail) {
if backoff.record_auth_failure(Instant::now(), &detail) {
warn!(
pr = entry.pr,
repo = %entry.repo,
"pr cleanup sweep: `gh` authentication failed: {detail}"
);
}
continue;
}
backoff.record_answer();
warn!(pr = entry.pr, repo = %entry.repo, "pr cleanup sweep: cannot read PR: {e:#}");
continue;
}
};
match sweep_decision(&entry, &view) {
SweepDecision::AlreadyCleaned => continue,
SweepDecision::NotMerged(reason) => {
info!(pr = entry.pr, repo = %entry.repo, "pr cleanup sweep: skipping — {reason}");
}
SweepDecision::Clean => {
let Some(fresh) = registry
.entry(&entry.repo, entry.pr)
.filter(OpenedPr::pending)
else {
info!(pr = entry.pr, repo = %entry.repo, "pr cleanup sweep: skipping — deferred or cleaned since it was read");
continue;
};
let req = CleanupRequest {
head_only: fresh.scope == CleanupScope::HeadOnly,
..req
};
let report =
super::run_with(gh, git, claims, landing, probe_dirt, &req, ownership).await;
if report.failed() {
warn!(
pr = entry.pr,
repo = %entry.repo,
"pr cleanup sweep: cleanup did not complete; leaving it pending:\n{}",
report.render()
);
continue;
}
if let Err(e) = registry.mark_cleaned(&entry.repo, entry.pr, Utc::now()) {
warn!(
pr = entry.pr,
repo = %entry.repo,
"pr cleanup sweep: cleaned #{} but could not stamp the registry: {e:#}; \
the next sweep will retry it",
entry.pr
);
continue;
}
info!(pr = entry.pr, repo = %entry.repo, "pr cleanup sweep: cleaned up after merge");
cleaned += 1;
}
}
}
cleaned
}
#[cfg(test)]
#[path = "sweep_tests.rs"]
mod sweep_tests;