Skip to content

Commit 5e04aa9

Browse files
Improved report to be more human-readable. (#11674)
Improved report to be more human-readable. Co-authored-by: alexey.kuznetsov <alexey.kuznetsov@datadoghq.com>
1 parent 4e8d69f commit 5e04aa9

2 files changed

Lines changed: 107 additions & 17 deletions

File tree

.github/scripts/dependency_age.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,12 @@ def is_instrumentation_path(relative_path: str) -> bool:
501501
# build summary of reverted/downgraded dependencies for PR descriptions
502502
# path_filter restricts the summary to lockfiles whose relative path matches,
503503
# so each PR (core vs instrumentation) only lists the dependencies it actually changes
504+
#
505+
# The summary is split into three on-screen sections, ordered by how urgently a human
506+
# must act on them:
507+
# 1. "Cannot verify age, reverted" — age could not be checked at all, needs manual resolution
508+
# 2. "<N>h cooldown, reverted" — too new and no older eligible version exists, so reverted to baseline
509+
# 3. "<N>h cooldown, updated to the previous version" — too new, downgraded to an older eligible version
504510
def build_validation_summary(
505511
*,
506512
violations_by_file: dict[str, list[tuple[str, str, int]]],
@@ -509,34 +515,60 @@ def build_validation_summary(
509515
min_age_hours: int,
510516
path_filter: Callable[[str], bool],
511517
) -> str:
512-
header = ["## Dependency age policy", ""]
513-
lines = list(header)
518+
unverified: list[str] = []
519+
cooldown_reverted: list[str] = []
520+
cooldown_updated: list[str] = []
514521
seen: set[str] = set()
522+
515523
for relative_path, replacements in replacements_by_file.items():
516524
if not path_filter(relative_path):
517525
continue
518526
baseline_coords = baseline_lockfiles.get(relative_path, set())
519527
for old_gav, (new_gav, hours_remaining) in replacements.items():
520-
if old_gav not in seen:
521-
seen.add(old_gav)
522-
if new_gav in baseline_coords:
523-
lines.append(f"- `{old_gav}` is {hours_remaining}h away from meeting {min_age_hours}h cooldown, reverted")
524-
else:
525-
new_version = new_gav.rsplit(":", 1)[1]
526-
lines.append(f"- `{old_gav}` is {hours_remaining}h away from meeting {min_age_hours}h cooldown, updated to `{new_version}`")
528+
if old_gav in seen:
529+
continue
530+
seen.add(old_gav)
531+
if new_gav in baseline_coords:
532+
# the only eligible version was the baseline, so this is effectively a revert
533+
cooldown_reverted.append(f"- `{old_gav}` is {hours_remaining}h away from meeting cooldown")
534+
else:
535+
new_version = new_gav.rsplit(":", 1)[1]
536+
cooldown_updated.append(f"- `{old_gav}` is {hours_remaining}h away from meeting cooldown, updated to `{new_version}`")
527537
for relative_path, entries in violations_by_file.items():
528538
if not path_filter(relative_path):
529539
continue
530540
for gav, kind, hours_remaining in entries:
531-
if gav not in seen:
532-
seen.add(gav)
533-
if kind == "unverified":
534-
lines.append(f"- `{gav}` — cannot verify age, reverted")
535-
else:
536-
lines.append(f"- `{gav}` is {hours_remaining}h away from meeting {min_age_hours}h cooldown, reverted")
537-
if len(lines) == len(header): # nothing matched the filter
541+
if gav in seen:
542+
continue
543+
seen.add(gav)
544+
if kind == "unverified":
545+
unverified.append(f"- `{gav}`")
546+
else:
547+
cooldown_reverted.append(f"- `{gav}` is {hours_remaining}h away from meeting cooldown")
548+
549+
blocks: list[str] = []
550+
if unverified:
551+
blocks.append(
552+
"### :warning: Cannot verify age, reverted\n\n"
553+
"The age of these dependencies could not be verified, so the lockfiles were reverted. "
554+
"**This needs to be resolved manually.**\n\n"
555+
+ "\n".join(sorted(unverified))
556+
)
557+
if cooldown_reverted:
558+
blocks.append(
559+
f"### {min_age_hours}h cooldown, reverted\n\n"
560+
"Too new and no older eligible version exists, so the lockfiles were reverted to the baseline.\n\n"
561+
+ "\n".join(sorted(cooldown_reverted))
562+
)
563+
if cooldown_updated:
564+
blocks.append(
565+
f"### {min_age_hours}h cooldown, updated to the previous version\n\n"
566+
"Too new, so an older eligible version was used instead.\n\n"
567+
+ "\n".join(sorted(cooldown_updated))
568+
)
569+
if not blocks: # nothing matched the filter
538570
return ""
539-
return "\n".join(lines)
571+
return "## Dependency age policy\n\n" + "\n\n".join(blocks)
540572

541573

542574
# replace specific coordinates in lockfiles (for version downgrades)

.github/scripts/tests/test_dependency_age.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,64 @@ def test_summary_is_empty_when_filter_matches_nothing(self) -> None:
424424
)
425425
self.assertEqual(empty, "")
426426

427+
def test_summary_groups_outcomes_into_sections(self) -> None:
428+
# one of each outcome: unverified, too-new revert, replacement-as-revert, replacement-as-update
429+
summary = dependency_age.build_validation_summary(
430+
violations_by_file={
431+
"core/gradle.lockfile": [
432+
("com.example:unverified-lib:1.0.0", "unverified", 0),
433+
("com.example:too-new-lib:2.0.0", "too_new", 5),
434+
],
435+
},
436+
replacements_by_file={
437+
"core/gradle.lockfile": {
438+
# eligible version equals the baseline -> effectively a revert
439+
"com.example:revert-lib:3.0.0": ("com.example:revert-lib:2.9.0", 7),
440+
# eligible version is newer than the baseline -> an update to a previous version
441+
"com.example:update-lib:4.0.0": ("com.example:update-lib:3.9.0", 9),
442+
},
443+
},
444+
baseline_lockfiles={
445+
"core/gradle.lockfile": {"com.example:revert-lib:2.9.0"},
446+
},
447+
min_age_hours=48,
448+
path_filter=lambda p: True,
449+
)
450+
451+
# three section headings present, in priority order
452+
unverified_idx = summary.index("### :warning: Cannot verify age, reverted")
453+
reverted_idx = summary.index("### 48h cooldown, reverted")
454+
updated_idx = summary.index("### 48h cooldown, updated to the previous version")
455+
self.assertLess(unverified_idx, reverted_idx)
456+
self.assertLess(reverted_idx, updated_idx)
457+
458+
# unverified entry lives under the manual-resolution section
459+
self.assertIn("**This needs to be resolved manually.**", summary)
460+
self.assertIn("- `com.example:unverified-lib:1.0.0`", summary)
461+
462+
# both the too-new violation and the revert-style replacement land in the reverted section
463+
reverted_block = summary[reverted_idx:updated_idx]
464+
self.assertIn("com.example:too-new-lib:2.0.0", reverted_block)
465+
self.assertIn("com.example:revert-lib:3.0.0", reverted_block)
466+
467+
# the update names the older version that was used instead
468+
updated_block = summary[updated_idx:]
469+
self.assertIn("com.example:update-lib:4.0.0", updated_block)
470+
self.assertIn("updated to `3.9.0`", updated_block)
471+
472+
def test_summary_omits_empty_sections(self) -> None:
473+
# only too-new violations -> only the "reverted" section should appear
474+
summary = dependency_age.build_validation_summary(
475+
violations_by_file={"core/gradle.lockfile": [("com.example:core-lib:2.0.0", "too_new", 5)]},
476+
replacements_by_file={},
477+
baseline_lockfiles={},
478+
min_age_hours=48,
479+
path_filter=lambda p: True,
480+
)
481+
self.assertIn("### 48h cooldown, reverted", summary)
482+
self.assertNotIn("Cannot verify age", summary)
483+
self.assertNotIn("updated to the previous version", summary)
484+
427485

428486
if __name__ == "__main__":
429487
unittest.main()

0 commit comments

Comments
 (0)