Add username
All checks were successful
Build and Publish Docker Image / test (push) Successful in 1m43s
Build and Publish Docker Image / build-and-push (push) Successful in 1m18s

This commit is contained in:
Andras Schmelczer 2026-08-08 21:32:27 +01:00
commit 07e582e2be
9 changed files with 358 additions and 192 deletions

View file

@ -1,6 +1,6 @@
use crate::config::UpdateStrategy;
use crate::version::VersionInfo;
use tracing::{debug, warn};
use tracing::debug;
pub fn create_selector(strategy: &UpdateStrategy) -> Box<dyn VersionSelector> {
match strategy {
@ -34,8 +34,6 @@ impl VersionSelector for LatestVersionSelector {
if let Some(ref selected) = latest {
debug!("Latest version selected: {}", selected.version);
} else {
warn!("No matching versions available");
}
latest
@ -55,29 +53,26 @@ impl VersionSelector for SmartPreviousMinorSelector {
get_filtered_and_sorted_matching_versions(available, current_prefix, current_suffix);
let latest = &versions.first()?.version;
let latest_line = (latest.major, latest.minor);
debug!("Latest version available: {}", latest);
let (target_major, max_minor) = if latest.minor == 0 {
if latest.major == 0 {
debug!("Cannot go to previous version of 0.0.x");
return None;
}
(latest.major - 1, None)
} else {
(latest.major, Some(latest.minor - 1))
};
// The target is the highest patch of the newest release line strictly
// below the latest one. Deriving that line by decrementing (`minor - 1`,
// or `major - 1` when the latest is a `.0`) assumes releases never skip a
// number, which registries routinely do: linuxserver/sonarr publishes 5.14
// and 4.0.x with no 5.13, so a decrement searched for a 5.x line that does
// not exist and returned nothing, stranding the image on its current tag.
// Since `versions` is sorted descending, the first entry below the latest
// line is exactly that.
let selected = versions
.iter()
.find(|v| {
v.version.major == target_major && max_minor.is_none_or(|m| v.version.minor <= m)
})
.find(|v| (v.version.major, v.version.minor) < latest_line)
.cloned();
if let Some(ref selected) = selected {
debug!("Selected version: {}", selected.version);
} else {
warn!("No matching versions available for previous minor strategy");
debug!("No release line below {} to fall back to", latest);
}
selected
@ -199,6 +194,81 @@ mod tests {
);
}
/// linuxserver/sonarr: the newest line is 5.14 with no 5.x below it, so the
/// previous line is the 4.0 series. Decrementing the minor used to look for a
/// nonexistent 5.13-or-lower and leave the image stuck on 4.0.15.
#[test]
fn test_previous_minor_steps_across_a_major_gap() {
let selector = create_selector(&UpdateStrategy::LatestPatchOfPreviousMinor);
let available = vec![
VersionInfo::from_tag("5.14").unwrap(),
VersionInfo::from_tag("4.0.19").unwrap(),
VersionInfo::from_tag("4.0.18").unwrap(),
VersionInfo::from_tag("4.0.15").unwrap(),
];
let target = selector.select_target_version(&available, None, None);
assert_eq!(
target.map(|v| v.version),
Some(Version::parse("4.0.19").unwrap())
);
}
/// linuxserver/qbittorrent: a stray legacy 14.3.9 tag sorts above every real
/// release, and there is no 14.x line below it. The previous line is 5.2.
#[test]
fn test_previous_minor_skips_an_isolated_outlier_line() {
let selector = create_selector(&UpdateStrategy::LatestPatchOfPreviousMinor);
let available = vec![
VersionInfo::from_tag("14.3.9").unwrap(),
VersionInfo::from_tag("5.2.3").unwrap(),
VersionInfo::from_tag("5.2.0").unwrap(),
VersionInfo::from_tag("5.1.4").unwrap(),
];
let target = selector.select_target_version(&available, None, None);
assert_eq!(
target.map(|v| v.version),
Some(Version::parse("5.2.3").unwrap())
);
}
/// Skipped minors (2.5 -> 2.3, no 2.4) must not strand the image either.
#[test]
fn test_previous_minor_handles_skipped_minors() {
let selector = create_selector(&UpdateStrategy::LatestPatchOfPreviousMinor);
let available = vec![
VersionInfo::from_tag("2.5.2").unwrap(),
VersionInfo::from_tag("2.3.5").unwrap(),
VersionInfo::from_tag("2.3.0").unwrap(),
];
let target = selector.select_target_version(&available, None, None);
assert_eq!(
target.map(|v| v.version),
Some(Version::parse("2.3.5").unwrap())
);
}
/// A single release line has nothing below it: staying put is correct, since
/// the strategy exists to keep one line behind the newest.
#[test]
fn test_previous_minor_without_an_older_line() {
let selector = create_selector(&UpdateStrategy::LatestPatchOfPreviousMinor);
let available = vec![
VersionInfo::from_tag("1.0.2").unwrap(),
VersionInfo::from_tag("1.0.1").unwrap(),
];
assert!(selector
.select_target_version(&available, None, None)
.is_none());
}
#[test]
fn test_cross_major_version_handling() {
let selector = create_selector(&UpdateStrategy::LatestPatchOfPreviousMinor);