diff --git a/.github/scripts/dependency_age.py b/.github/scripts/dependency_age.py index ff3a46adde4..67d9a76152f 100644 --- a/.github/scripts/dependency_age.py +++ b/.github/scripts/dependency_age.py @@ -439,7 +439,7 @@ def validate_lockfiles(args: argparse.Namespace) -> int: elif published_at > cutoff: hours_remaining = int((published_at - cutoff).total_seconds() / 3600) + 1 group_id, artifact_id, version = gav.split(":", 2) - baseline_version = next((c[len(f"{group_id}:{artifact_id}:"):] for c in baseline_coords if c.startswith(f"{group_id}:{artifact_id}:")), None) + baseline_version = highest_baseline_version(baseline_coords, group_id, artifact_id) eligible = find_eligible_version( group_id=group_id, artifact_id=artifact_id, too_new_version=version, baseline_version=baseline_version, @@ -678,6 +678,15 @@ def fetch_available_versions(group_id: str, artifact_id: str, repo_urls: list[st return [] +# select the highest baseline version of group:artifact present in a lockfile. +def highest_baseline_version(baseline_coords: set[str], group_id: str, artifact_id: str) -> str | None: + prefix = f"{group_id}:{artifact_id}:" + versions = [coord[len(prefix):] for coord in baseline_coords if coord.startswith(prefix)] + if not versions: + return None + return max(versions, key=_version_sort_key) + + # for a too-new coordinate, walk backward through available versions to find the newest one # that meets the age cutoff and is newer than the baseline version def find_eligible_version( diff --git a/.github/scripts/tests/test_dependency_age.py b/.github/scripts/tests/test_dependency_age.py index 6813497e11a..0ceef182d71 100644 --- a/.github/scripts/tests/test_dependency_age.py +++ b/.github/scripts/tests/test_dependency_age.py @@ -1,3 +1,6 @@ +import argparse +import contextlib +import io import json import os import re @@ -6,7 +9,9 @@ import sys import tempfile import unittest +from datetime import datetime, timezone from pathlib import Path +from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[3] SCRIPT = REPO_ROOT / ".github/scripts/dependency_age.py" @@ -469,6 +474,24 @@ def test_summary_groups_outcomes_into_sections(self) -> None: self.assertIn("com.example:update-lib:4.0.0", updated_block) self.assertIn("updated to `3.9.0`", updated_block) + def test_highest_baseline_version_picks_newest_of_coexisting_pins(self) -> None: + # A single lockfile can pin the same artifact at several versions (one per Gradle + # configuration). The baseline should be the newest so that only versions higher than + # the highest existing version are considered upgrades. + baseline_coords = { + "ch.qos.logback:logback-core:1.1.11", + "ch.qos.logback:logback-core:1.2.13", + "ch.qos.logback:logback-core:1.5.34", + "com.example:unrelated:9.9.9", + } + self.assertEqual( + dependency_age.highest_baseline_version(baseline_coords, "ch.qos.logback", "logback-core"), + "1.5.34", + ) + self.assertIsNone( + dependency_age.highest_baseline_version(baseline_coords, "ch.qos.logback", "logback-classic") + ) + def test_summary_omits_empty_sections(self) -> None: # only too-new violations -> only the "reverted" section should appear summary = dependency_age.build_validation_summary(