diff --git a/.github/scripts/dependency_age.py b/.github/scripts/dependency_age.py index 9c0a77a8bc2..1a54e12c074 100644 --- a/.github/scripts/dependency_age.py +++ b/.github/scripts/dependency_age.py @@ -13,7 +13,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any +from typing import Any, Callable GRADLE_VERSIONS_URL = "https://services.gradle.org/versions/all" @@ -428,25 +428,49 @@ def validate_lockfiles(args: argparse.Namespace) -> int: print(f"::warning file={relative_path}::{gav}: {'Cannot verify age' if kind == 'unverified' else 'Too new'}. Reverted lockfile to baseline.") reverted_files = len(violations_by_file) - summary = build_validation_summary(violations_by_file=violations_by_file, replacements_by_file=replacements_by_file, baseline_lockfiles=baseline_lockfiles, min_age_hours=args.min_age_hours) - emit_outputs({"cutoff_at": format_datetime(cutoff), "reverted_files": reverted_files, "summary": summary}, args.github_output) + summary_instrumentation = build_validation_summary(violations_by_file=violations_by_file, replacements_by_file=replacements_by_file, baseline_lockfiles=baseline_lockfiles, min_age_hours=args.min_age_hours, path_filter=is_instrumentation_path) + summary_core = build_validation_summary(violations_by_file=violations_by_file, replacements_by_file=replacements_by_file, baseline_lockfiles=baseline_lockfiles, min_age_hours=args.min_age_hours, path_filter=lambda path: not is_instrumentation_path(path)) + emit_outputs( + { + "cutoff_at": format_datetime(cutoff), + "reverted_files": reverted_files, + "summary_core": summary_core, + "summary_instrumentation": summary_instrumentation, + }, + args.github_output, + ) print(f"Validated {len(changed)} changed coordinate(s) across {len(changed_by_file)} lockfile(s). {reverted_files} lockfile(s) reverted.") return 0 +# instrumentation lockfiles live under these prefixes and ship in a separate PR from core modules. +# Keep in sync with the file split in .github/workflows/update-gradle-dependencies.yaml +INSTRUMENTATION_PATH_PREFIXES = ("dd-smoke-tests/", "dd-java-agent/instrumentation/") + + +# classify a lockfile path as belonging to the instrumentation PR (vs the core modules PR) +def is_instrumentation_path(relative_path: str) -> bool: + normalized = relative_path.replace(os.sep, "/") + return normalized.startswith(INSTRUMENTATION_PATH_PREFIXES) + + # build summary of reverted/downgraded dependencies for PR descriptions +# path_filter restricts the summary to lockfiles whose relative path matches, +# so each PR (core vs instrumentation) only lists the dependencies it actually changes def build_validation_summary( *, violations_by_file: dict[str, list[tuple[str, str, int]]], replacements_by_file: dict[str, dict[str, tuple[str, int]]], baseline_lockfiles: dict[str, set[str]], min_age_hours: int, + path_filter: Callable[[str], bool], ) -> str: - if not violations_by_file and not replacements_by_file: - return "" - lines = [f"## Dependency age policy", ""] + header = ["## Dependency age policy", ""] + lines = list(header) seen: set[str] = set() for relative_path, replacements in replacements_by_file.items(): + if not path_filter(relative_path): + continue baseline_coords = baseline_lockfiles.get(relative_path, set()) for old_gav, (new_gav, hours_remaining) in replacements.items(): if old_gav not in seen: @@ -456,7 +480,9 @@ def build_validation_summary( else: new_version = new_gav.rsplit(":", 1)[1] lines.append(f"- `{old_gav}` is {hours_remaining}h away from meeting {min_age_hours}h cooldown, updated to `{new_version}`") - for entries in violations_by_file.values(): + for relative_path, entries in violations_by_file.items(): + if not path_filter(relative_path): + continue for gav, kind, hours_remaining in entries: if gav not in seen: seen.add(gav) @@ -464,6 +490,8 @@ def build_validation_summary( lines.append(f"- `{gav}` — cannot verify age, reverted") else: lines.append(f"- `{gav}` is {hours_remaining}h away from meeting {min_age_hours}h cooldown, reverted") + if len(lines) == len(header): # nothing matched the filter + return "" return "\n".join(lines) diff --git a/.github/scripts/tests/test_dependency_age.py b/.github/scripts/tests/test_dependency_age.py index 1bf372201ee..6cd2bd2cf7f 100644 --- a/.github/scripts/tests/test_dependency_age.py +++ b/.github/scripts/tests/test_dependency_age.py @@ -3,13 +3,20 @@ import re import shutil import subprocess +import sys import tempfile import unittest from pathlib import Path - REPO_ROOT = Path(__file__).resolve().parents[3] SCRIPT = REPO_ROOT / ".github/scripts/dependency_age.py" + +# dependency_age.py is a loose script (not a package); add its dir to sys.path +# so its helpers can be imported and unit-tested. +sys.path.insert(0, str(SCRIPT.parent)) +import dependency_age + + FIXTURES = Path(__file__).resolve().parent / "fixtures" NOW = "2026-04-24T12:00:00Z" OUTPUT_PATTERN = re.compile( @@ -373,6 +380,50 @@ def test_reverts_lockfile_when_metadata_override_has_invalid_timestamp(self) -> self.assertEqual(outputs["reverted_files"], "1") self.assertEqual((current_dir / "module/gradle.lockfile").read_text(encoding="utf-8"), baseline_content) + def test_is_instrumentation_path_classifies_prefixes(self) -> None: + self.assertTrue(dependency_age.is_instrumentation_path("dd-smoke-tests/foo/gradle.lockfile")) + self.assertTrue(dependency_age.is_instrumentation_path("dd-java-agent/instrumentation/bar/gradle.lockfile")) + # core modules are not instrumentation + self.assertFalse(dependency_age.is_instrumentation_path("dd-trace-core/gradle.lockfile")) + self.assertFalse(dependency_age.is_instrumentation_path("dd-java-agent/agent-bootstrap/gradle.lockfile")) + # real sibling modules that share the "dd-java-agent/instrumentation" stem but are + # NOT under the "dd-java-agent/instrumentation/" prefix — the trailing slash excludes them + self.assertFalse(dependency_age.is_instrumentation_path("dd-java-agent/instrumentation-testing/gradle.lockfile")) + self.assertFalse(dependency_age.is_instrumentation_path("dd-java-agent/instrumentation-annotation-processor/gradle.lockfile")) + + def _summary(self, *, path_filter) -> str: + # one too-new violation in a core module, one in an instrumentation module + return dependency_age.build_validation_summary( + violations_by_file={ + "dd-trace-core/gradle.lockfile": [("com.example:core-lib:2.0.0", "too_new", 5)], + "dd-java-agent/instrumentation/foo/gradle.lockfile": [("com.example:inst-lib:3.0.0", "too_new", 7)], + }, + replacements_by_file={}, + baseline_lockfiles={}, + min_age_hours=48, + path_filter=path_filter, + ) + + def test_core_summary_excludes_instrumentation_entries(self) -> None: + summary = self._summary(path_filter=lambda p: not dependency_age.is_instrumentation_path(p)) + self.assertIn("com.example:core-lib:2.0.0", summary) + self.assertNotIn("com.example:inst-lib:3.0.0", summary) + + def test_instrumentation_summary_excludes_core_entries(self) -> None: + summary = self._summary(path_filter=dependency_age.is_instrumentation_path) + self.assertIn("com.example:inst-lib:3.0.0", summary) + self.assertNotIn("com.example:core-lib:2.0.0", summary) + + def test_summary_is_empty_when_filter_matches_nothing(self) -> None: + empty = dependency_age.build_validation_summary( + violations_by_file={"dd-trace-core/gradle.lockfile": [("com.example:core-lib:2.0.0", "too_new", 5)]}, + replacements_by_file={}, + baseline_lockfiles={}, + min_age_hours=48, + path_filter=dependency_age.is_instrumentation_path, # nothing under instrumentation + ) + self.assertEqual(empty, "") + if __name__ == "__main__": unittest.main() diff --git a/.github/workflows/update-gradle-dependencies.yaml b/.github/workflows/update-gradle-dependencies.yaml index 737686dbdd0..f059041e16f 100644 --- a/.github/workflows/update-gradle-dependencies.yaml +++ b/.github/workflows/update-gradle-dependencies.yaml @@ -108,7 +108,7 @@ jobs: if: steps.check-core-changes.outputs.commit_changes == 'true' env: GH_TOKEN: ${{ steps.octo-sts.outputs.token }} - PR_SUMMARY: ${{ steps.validate-lockfiles.outputs.summary }} + PR_SUMMARY: ${{ steps.validate-lockfiles.outputs.summary_core }} run: | gh pr create --title "Update Gradle dependencies" \ --base master \ @@ -168,7 +168,7 @@ jobs: if: steps.check-instrumentation-changes.outputs.commit_changes == 'true' env: GH_TOKEN: ${{ steps.octo-sts.outputs.token }} - PR_SUMMARY: ${{ steps.validate-lockfiles.outputs.summary }} + PR_SUMMARY: ${{ steps.validate-lockfiles.outputs.summary_instrumentation }} run: | gh pr create --title "Update instrumentation Gradle dependencies" \ --base master \