Fix Docker Hub authentication and reject build-id tags as candidates

Docker Hub answers with both token and access_token, which made the
serde alias fail as a duplicate field, so every Hub image reported that
no token was returned. Candidates now also have to match the shape of
the running tag; cert-manager v1.14.5 was otherwise offered an upgrade
to the build id 608111629.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-03 20:24:50 +02:00
parent 214cd1506b
commit b161bf9cbd
2 changed files with 59 additions and 3 deletions

View File

@ -89,18 +89,24 @@ fn is_prerelease(suffix: &str) -> bool {
.any(|m| s.starts_with(m) || s.contains(&format!("-{m}")))
}
/// Tags from the registry that are newer than `current`, oldest first. Only tags with the
/// same suffix (`-rootless`, `-debian-12` …) are considered, so the variant stays the same.
/// Tags from the registry that are newer than `current`, oldest first.
///
/// A candidate has to be shaped like the running tag: the same variant suffix
/// (`-rootless`, `-debian-12` …), the same `v` prefix and the same number of version
/// parts. That keeps build ids and dates (`608111629`, `20260417`) out of the suggestion.
pub fn newer_tags(current: &str, available: &[String]) -> Vec<String> {
let Some((now, suffix)) = version_parts(current) else {
return Vec::new();
};
let prefixed = current.starts_with('v');
let mut newer: Vec<(Vec<u64>, String)> = available
.iter()
.filter_map(|t| {
let (v, s) = version_parts(t)?;
(s == suffix
&& !is_prerelease(&s)
&& t.starts_with('v') == prefixed
&& v.len() == now.len()
&& cmp_version(&v, &now) == std::cmp::Ordering::Greater)
.then_some((v, t.clone()))
})
@ -218,6 +224,31 @@ mod tests {
assert_eq!(newest_tag("latest", &tags), None, "no version to compare");
}
#[test]
fn ignores_tags_that_are_not_shaped_like_the_running_one() {
// build ids and dates are numerically larger but are not a newer release
let tags: Vec<String> = [
"v1.14.5",
"608111629",
"20260417",
"v1.15.0",
"v1.14.6",
"1.16.0",
]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(newer_tags("v1.14.5", &tags), vec!["v1.14.6", "v1.15.0"]);
assert_eq!(newest_tag("v1.14.5", &tags).as_deref(), Some("v1.15.0"));
// the number of version parts has to match, so 1.11 does not jump to 1.11.0.1
let tags: Vec<String> = ["1.11", "1.12", "1.12.0", "1.12.0.1"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(newer_tags("1.11", &tags), vec!["1.12"]);
}
#[test]
fn compares_versions_by_number_not_by_text() {
let tags: Vec<String> = ["1.9.0", "1.10.0", "1.10", "2.0.0"]
@ -225,7 +256,11 @@ mod tests {
.map(|s| s.to_string())
.collect();
assert_eq!(newest_tag("1.9.0", &tags).as_deref(), Some("2.0.0"));
assert_eq!(newer_tags("1.9.0", &tags), vec!["1.10", "1.10.0", "2.0.0"]);
assert_eq!(
newer_tags("1.9.0", &tags),
vec!["1.10.0", "2.0.0"],
"1.10 has fewer parts"
);
assert_eq!(
newest_tag("v0.6.3", &["v0.7.0".to_string(), "v0.6.4".to_string()]).as_deref(),
Some("v0.7.0")