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
42 changes: 35 additions & 7 deletions .github/scripts/dependency_age.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -456,14 +480,18 @@ 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)
if kind == "unverified":
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)


Expand Down
53 changes: 52 additions & 1 deletion .github/scripts/tests/test_dependency_age.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()
4 changes: 2 additions & 2 deletions .github/workflows/update-gradle-dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down