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
45 changes: 43 additions & 2 deletions crates/core/src/retention/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
//! it was published — so a fresh push is not cold on its first run),
//! `prerelease_superseded_for_days` (a prerelease or snapshot, once the
//! first listed release of its package that orders after it has been
//! published that long), `unlisted_for_days` (unlisted that long, by
//! published that long, counted from the prerelease's own publication
//! when that came later), `unlisted_for_days` (unlisted that long, by
//! `unlisted_at`) and `max_versions_per_package` (not among the newest N
//! of its partition of the pool, listed or not).
//! 5. Protection overrides selection. Otherwise the highest-severity action
Expand Down Expand Up @@ -445,7 +446,10 @@ fn triggers_match(
any_trigger = true;
// A release is never superseded; a prerelease only by a release.
let release = inputs.superseded_by?;
if days_since(now, release.created_at) < days as i64 {
// The clock starts when both exist: a prerelease published after its
// release (a lagging CI bump, a maintenance line) has its N days too.
let since = release.created_at.max(cand.created_at);
if days_since(now, since) < days as i64 {
return None;
}
reason.prerelease_superseded_for_days = Some(days);
Expand Down Expand Up @@ -2454,6 +2458,43 @@ mod tests {
assert!(d.is_empty(), "{d:?}");
}

#[test]
fn a_prerelease_published_after_its_release_has_its_days_too() {
// 2.3.0 has been out for 30 days; CI's lagging bump published
// 2.3.0-ci.500 today and 2.3.0-ci.499 20 days ago. A maintenance
// line's 1.9.1-rc.1 came out 3 days ago, months after 2.0.0.
let cands = vec![
comp(1, "lib", "1.9.1-rc.1", 3, None),
comp(2, "lib", "2.0.0", 200, None),
comp(3, "lib", "2.3.0-ci.1", 60, None),
comp(4, "lib", "2.3.0", 30, None),
comp(5, "lib", "2.3.0-ci.499", 20, None),
comp(6, "lib", "2.3.0-ci.500", 0, None),
];
let d = evaluate(
&fmt(),
now(),
&hard_delete(superseded(14)),
&Selectors::none(),
&cands,
);
// ci.1 counts from 2.3.0 (30 days), ci.499 from itself (20 days);
// ci.500 and rc.1 are younger than 14 days.
assert_eq!(deleted_versions(&d), vec!["2.3.0-ci.1", "2.3.0-ci.499"]);
let late = d.iter().find(|d| d.version == "2.3.0-ci.499").unwrap();
assert_eq!(late.reason.superseded_by.as_deref(), Some("2.3.0"));

// 25 days: ci.1 still goes, ci.499 (20 days old) stays.
let d = evaluate(
&fmt(),
now(),
&hard_delete(superseded(25)),
&Selectors::none(),
&cands,
);
assert_eq!(deleted_versions(&d), vec!["2.3.0-ci.1"]);
}

#[test]
fn unlisted_for_days_deletes_versions_unlisted_long_enough() {
let mut long_ago = unlisted(comp(1, "lib", "1.0.0", 400, None));
Expand Down
6 changes: 5 additions & 1 deletion crates/core/tests/retention_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,7 +1437,8 @@ async fn a_scheduled_policy_runs_next_at_its_schedule() {

/// #185: `unlisted_for_days` deletes versions unlisted long enough, by the
/// time the unlisting recorded; `prerelease_superseded_for_days` deletes a
/// prerelease once its release has been out long enough.
/// prerelease once its release has been out long enough, and (#757) once it
/// has itself been out that long when it was published after its release.
#[tokio::test]
async fn unlisted_and_superseded_versions_go_after_their_days() {
let pool = pool().await;
Expand All @@ -1452,6 +1453,8 @@ async fn unlisted_and_superseded_versions_go_after_their_days() {
("old", "1.2.0", 400),
("app", "2.0.0-ci.1", 60),
("app", "2.0.0", 30),
// Published after 2.0.0 (a lagging CI bump): kept for its 14 days.
("app", "2.0.0-ci.2", 2),
("app", "2.1.0-ci.1", 5),
]
.into_iter()
Expand Down Expand Up @@ -1500,6 +1503,7 @@ async fn unlisted_and_superseded_versions_go_after_their_days() {
assert!(component_exists(&pool, repo.id, "old", "1.2.0").await);
assert!(!component_exists(&pool, repo.id, "app", "2.0.0-ci.1").await);
assert!(component_exists(&pool, repo.id, "app", "2.0.0").await);
assert!(component_exists(&pool, repo.id, "app", "2.0.0-ci.2").await);
assert!(component_exists(&pool, repo.id, "app", "2.1.0-ci.1").await);
let reasons: Vec<(String, Value)> = sqlx::query_as(
"SELECT rule_id, reason FROM retention_audit WHERE repository_id = $1 ORDER BY rule_id",
Expand Down
13 changes: 11 additions & 2 deletions crates/server/tests/admin_retention.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1439,8 +1439,9 @@ async fn preview_shows_the_new_triggers_decisions_and_the_next_runs() {
],
)
.await;
// 1.0.0 was published 20 days ago, the CI builds before it; 1.1.0 has
// been unlisted for 100 days.
// 1.0.0 was published 20 days ago, the CI builds 30 days ago, before it
// (a prerelease published after its release keeps its days, #757);
// 1.1.0 has been unlisted for 100 days.
sqlx::query(
"UPDATE component SET created_at = now() - interval '20 days' \
WHERE repository_id = $1 AND version = '1.0.0'",
Expand All @@ -1449,6 +1450,14 @@ async fn preview_shows_the_new_triggers_decisions_and_the_next_runs() {
.execute(&srv.pool)
.await
.unwrap();
sqlx::query(
"UPDATE component SET created_at = now() - interval '30 days' \
WHERE repository_id = $1 AND version LIKE '1.0.0-ci.%'",
)
.bind(repo_id)
.execute(&srv.pool)
.await
.unwrap();
sqlx::query(
"UPDATE component SET listed = false WHERE repository_id = $1 AND version = '1.1.0'",
)
Expand Down
5 changes: 5 additions & 0 deletions docs/changes/2.2.0/757.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Retention's `prerelease_superseded_for_days` counted only from the
superseding release's publication, so a prerelease published after its
release (a lagging CI version bump, a maintenance line's `1.9.1-rc.1` after
`2.0.0`) was deleted on the next run. Its clock now starts at the later of
the two publications: a prerelease always has its N days.
2 changes: 1 addition & 1 deletion docs/retention.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ needs a trigger; a rule with protections only is refused.
| --- | --- |
| `older_than_days` | published at least N days ago. |
| `not_downloaded_for_days` | not downloaded for N days; a version never downloaded counts from its publication. |
| `prerelease_superseded_for_days` | that is a prerelease (or a snapshot) once its package has had, for N days, a **listed release that comes after it**: for SemVer, a release of the same or a higher version, so `2.3.0-ci.417` goes 14 days after `2.3.0` (or `2.4.0`) was published. The clock starts at the first such release's publication. A release is never selected. |
| `prerelease_superseded_for_days` | that is a prerelease (or a snapshot) once its package has had, for N days, a **listed release that comes after it**: for SemVer, a release of the same or a higher version, so `2.3.0-ci.417` goes 14 days after `2.3.0` (or `2.4.0`) was published. The clock starts at the first such release's publication, or at the prerelease's own when it was published later (a lagging CI bump, a maintenance line's `1.9.1-rc.1` after `2.0.0`): a prerelease always has its N days. A release is never selected. |
| `unlisted_for_days` | that has been unlisted for N days (a listed version never). Only on `hard_delete` rules: the second step of "unlist, then delete later". |
| `max_versions_per_package` | that is not among the N newest versions of its package (listed or not) the rule's filters admit. A pure count: no age needed. |

Expand Down
Loading