chadtmiller.com/knotless
git that lives in your Atmosphere account
7
//! Which of a runner's checks of a repository it deletes: nothing deletes
//! a check otherwise, and most are soon of no use. A check still running,
//! or finished within the grace, stays; of the rest, a check goes when it
//! is:
//!
//! - a merge-queue batch's run, whose pulls have landed or been ejected;
//! - a pull's run, once the pull is closed or merged, or a newer run of
//! the same pull and workflow was filed;
//! - a run on a ref other than the default branch, once that ref is gone
//! or a newer run of the same ref and workflow was filed;
//! - a run on the default branch older than what is kept, unless it is
//! the newest of its workflow there.
//!
//! A push that takes a pull's or a batch's pass copies its log first (see
//! [`crate::check::copied`]), so the default branch's checks stand alone.
use crate::check::{Check, status};
use std::collections::{HashMap, HashSet};
use std::time::{Duration, SystemTime};
/// What a repository is now, to judge its checks by.
pub struct Now<'a> {
/// The open pulls, by AT-URI.
pub open: &'a HashSet<String>,
/// The refs that point anywhere, in full.
pub refs: &'a HashSet<String>,
/// The default branch, in full.
pub main: &'a str,
pub at: SystemTime,
/// How long a check that finished stays whatever it is.
pub grace: Duration,
/// How long the default branch's checks stay.
pub keep: Duration,
}
/// What a check is a run of, for the newest of each to be found.
#[derive(PartialEq, Eq, Hash)]
enum Of<'a> {
Batch,
Pull(&'a str, &'a str),
Ref(&'a str, &'a str),
}
fn of(check: &Check) -> Of<'_> {
let workflow = check.workflow.as_str();
match &check.pull {
_ if !check.batch.is_empty() => Of::Batch,
Some(pull) => Of::Pull(pull, workflow),
None => Of::Ref(&check.ref_name, workflow),
}
}
/// When `stamp`, an RFC 3339 time, was: never, if it does not read as one.
fn when(stamp: &str) -> SystemTime {
humantime::parse_rfc3339_weak(stamp.trim_end_matches('Z')).unwrap_or(SystemTime::UNIX_EPOCH)
}
/// How long before `now` the time `stamp` was.
fn age(now: SystemTime, stamp: &str) -> Duration {
now.duration_since(when(stamp)).unwrap_or_default()
}
/// The keys of the `checks`, by key, that go, as the repository is `now`.
pub fn pruned(checks: &[(String, Check)], now: &Now<'_>) -> Vec<String> {
let mut newest: HashMap<Of<'_>, &str> = HashMap::new();
for (_, check) in checks {
let started = newest.entry(of(check)).or_default();
*started = (*started).max(&check.started_at);
}
let goes = |check: &Check| {
let finished = check.finished_at.as_deref().unwrap_or(&check.started_at);
if status::going(&check.status) || age(now.at, finished) < now.grace {
return false;
}
let superseded = newest[&of(check)] != check.started_at;
match of(check) {
Of::Batch => true,
Of::Pull(pull, _) => superseded || !now.open.contains(pull),
Of::Ref(name, _) if name == now.main => {
superseded && age(now.at, &check.started_at) >= now.keep
}
Of::Ref(name, _) => superseded || !now.refs.contains(name),
}
};
let gone = checks.iter().filter(|(_, check)| goes(check));
gone.map(|(rkey, _)| rkey.clone()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn check(of: &str, workflow: &str, started: &str, finished: Option<&str>) -> Check {
let mut check: Check = serde_json::from_value(serde_json::json!({
"repo": {"uri": "at://did:plc:o/dev.knotless.repo/r", "key": "k"},
"ref": "refs/heads/main",
"commit": "c",
"workflow": workflow,
"status": status::SUCCESS,
"startedAt": started,
}))
.unwrap();
check.finished_at = finished.map(str::to_string);
match of {
"batch" => {
check.batch =
serde_json::from_str(r#"[{"pull": "at://p", "commit": "c"}]"#).unwrap()
}
pull if pull.starts_with("at://") => check.pull = Some(pull.into()),
name => check.ref_name = name.into(),
}
check
}
#[test]
fn prunes_what_is_of_no_more_use() {
let day = |d: u32| format!("2026-10-{d:02}T12:00:00.000Z");
let done = |of: &str, wf: &str, d: u32| check(of, wf, &day(d), Some(&day(d)));
let all = [
// A batch's run goes; one finished within the grace stays.
("batch-old", done("batch", "ci", 1)),
("batch-new", done("batch", "ci", 25)),
// An open pull keeps its newest run of each workflow.
("open-1", done("at://open", "ci", 2)),
("open-2", done("at://open", "ci", 3)),
("open-docs", done("at://open", "docs", 2)),
// A closed or merged one keeps none.
("merged", done("at://merged", "ci", 3)),
// A branch keeps its newest of each, while it is there.
("topic-1", done("refs/heads/topic", "ci", 2)),
("topic-2", done("refs/heads/topic", "ci", 3)),
("gone", done("refs/heads/gone", "ci", 3)),
// Main keeps what is newer than its window, and its newest of
// each workflow whatever its age.
("main-old", done("refs/heads/main", "ci", 1)),
("main-kept", done("refs/heads/main", "ci", 20)),
("main-newest", done("refs/heads/main", "ci", 21)),
("main-only", done("refs/heads/main", "deploy", 1)),
// One still running stays, as does one never finished but
// started within the grace.
("running", {
let mut c = check("batch", "ci", &day(1), None);
c.status = status::RUNNING.into();
c
}),
("unfinished", check("batch", "ci", &day(25), None)),
];
let checks: Vec<(String, Check)> = all.into_iter().map(|(k, c)| (k.into(), c)).collect();
let open = HashSet::from(["at://open".to_string()]);
let refs = HashSet::from(["refs/heads/main".into(), "refs/heads/topic".into()]);
let now = Now {
open: &open,
refs: &refs,
main: "refs/heads/main",
at: when("2026-10-25T13:00:00Z"),
grace: Duration::from_secs(86_400),
keep: Duration::from_secs(10 * 86_400),
};
let gone = pruned(&checks, &now);
assert_eq!(
gone,
[
"batch-old",
"open-1",
"merged",
"topic-1",
"gone",
"main-old"
]
);
// A time that does not read as one is long ago.
assert_eq!(when("then"), SystemTime::UNIX_EPOCH);
}
}