Skip to content

Commit fd5b308

Browse files
Merge branch 'master' into alexeyk/revert-grizzly-bom-fix
2 parents 9fb3095 + 25bb679 commit fd5b308

207 files changed

Lines changed: 4444 additions & 1807 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/migrate-groovy-to-java/SKILL.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ Migrate test Groovy files to Java using JUnit 5
1212
2. Convert Groovy files to Java using JUnit 5
1313
3. Make sure the tests are still passing after migration and that the test count has not changed
1414
4. Remove Groovy files
15-
5. Add the migrated module path(s) to `.github/g2j-migrated-modules.txt`
1615

1716
When converting Groovy code to Java code, make sure that:
1817
- The Java code generated is compatible with JDK 8

.github/CODEOWNERS

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@
2121
/dd-smoke-tests/vertx-*/ @DataDog/apm-idm-java
2222
/dd-smoke-tests/wildfly/ @DataDog/apm-idm-java
2323

24-
# @DataDog/apm-release-platform
25-
/.gitlab/ @DataDog/apm-release-platform
26-
/.gitlab-ci.yml @DataDog/apm-release-platform
27-
2824
# @DataDog/apm-sdk-capabilities-java
2925
/dd-java-agent/agent-otel @DataDog/apm-sdk-capabilities-java
3026
/dd-smoke-tests/log-injection @DataDog/apm-sdk-capabilities-java
@@ -56,6 +52,8 @@
5652

5753
# @DataDog/apm-lang-platform-java
5854
/.github/ @DataDog/apm-lang-platform-java
55+
/.gitlab/ @DataDog/apm-lang-platform-java
56+
/.gitlab-ci.yml @DataDog/apm-lang-platform-java
5957
/benchmark/ @DataDog/apm-lang-platform-java
6058
/components/ @DataDog/apm-lang-platform-java
6159
/dd-java-agent/instrumentation/java/ @DataDog/apm-lang-platform-java

.github/scripts/dependency_age.py

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import json
5+
import os
6+
import re
7+
import sys
8+
import urllib.parse
9+
import urllib.request
10+
from dataclasses import dataclass
11+
from datetime import datetime, timedelta, timezone
12+
from pathlib import Path
13+
from typing import Any
14+
15+
16+
GRADLE_VERSIONS_URL = "https://services.gradle.org/versions/all"
17+
MAVEN_SEARCH_URL = "https://search.maven.org/solrsearch/select"
18+
DEFAULT_MIN_AGE_HOURS = 48
19+
20+
21+
22+
@dataclass(frozen=True)
23+
class Candidate:
24+
version: str
25+
published_at: datetime
26+
27+
28+
# Entry point for GitHub Actions workflows
29+
# select-gradle: get newest Gradle release that is at least MIN_DEPENDENCY_AGE_HOURS hours old
30+
# select-maven: get newest Maven artifact release that is at least MIN_DEPENDENCY_AGE_HOURS hours old
31+
def parse_args() -> argparse.Namespace:
32+
parser = argparse.ArgumentParser(description="Dependency age helpers for GitHub workflows.")
33+
subparsers = parser.add_subparsers(dest="command", required=True)
34+
35+
gradle = subparsers.add_parser("select-gradle", help="Select the newest eligible Gradle release.")
36+
add_common_selection_args(gradle)
37+
gradle.add_argument("--versions-url", default=GRADLE_VERSIONS_URL)
38+
gradle.add_argument("--versions-file")
39+
40+
maven = subparsers.add_parser("select-maven", help="Select the newest eligible Maven artifact release.")
41+
add_common_selection_args(maven)
42+
maven.add_argument("--group-id", required=True)
43+
maven.add_argument("--artifact-id", required=True)
44+
maven.add_argument("--search-url", default=MAVEN_SEARCH_URL)
45+
maven.add_argument("--search-response-file")
46+
maven.add_argument(
47+
"--prerelease-pattern",
48+
action="append",
49+
default=[],
50+
help="Case-insensitive regex fragment used to exclude prerelease versions.",
51+
)
52+
53+
return parser.parse_args()
54+
55+
56+
# add shared args used by select-gradle and select-maven
57+
def add_common_selection_args(parser: argparse.ArgumentParser) -> None:
58+
parser.add_argument("--min-age-hours", type=int, default=default_min_age_hours())
59+
parser.add_argument("--now")
60+
parser.add_argument("--current-version", default=None)
61+
parser.add_argument("--github-output", default=None)
62+
63+
64+
# get MIN_DEPENDENCY_AGE_HOURS from environment variable; default is 48 hours
65+
def default_min_age_hours() -> int:
66+
try:
67+
return int(os.environ.get("MIN_DEPENDENCY_AGE_HOURS", DEFAULT_MIN_AGE_HOURS))
68+
except ValueError:
69+
return DEFAULT_MIN_AGE_HOURS
70+
71+
72+
# return input as a datetime object; default to current UTC time
73+
def now_utc(raw: str | None) -> datetime:
74+
if raw:
75+
return parse_datetime(raw)
76+
return datetime.now(timezone.utc)
77+
78+
79+
# now_utc helper to parse input as a datetime object; used for Gradle and Maven timestamps
80+
def parse_datetime(value: Any) -> datetime:
81+
if isinstance(value, datetime):
82+
return value.astimezone(timezone.utc)
83+
if isinstance(value, (int, float)):
84+
timestamp = float(value)
85+
if timestamp > 10_000_000_000:
86+
timestamp /= 1000.0
87+
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
88+
if value is None:
89+
raise ValueError("timestamp is required")
90+
91+
text = str(value).strip()
92+
if not text:
93+
raise ValueError("timestamp is empty")
94+
95+
# Gradle buildTime compact format: 20260423130000+0000
96+
try:
97+
return datetime.strptime(text, "%Y%m%d%H%M%S%z").astimezone(timezone.utc)
98+
except ValueError:
99+
pass
100+
101+
# ISO 8601: normalise Z and +HHMM → +HH:MM for fromisoformat
102+
text = re.sub(r"([+-])(\d{2})(\d{2})$", r"\1\2:\3", text.replace("Z", "+00:00"))
103+
return datetime.fromisoformat(text).astimezone(timezone.utc)
104+
105+
106+
# normalize datetime to YYYY-MM-DDTHH:MM:SSZ for GitHub Actions outputs
107+
def format_datetime(value: datetime) -> str:
108+
return value.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
109+
110+
111+
# normalize datetime to YYYY-MM-DD date for more readable PR comment outputs
112+
def format_date(value: datetime) -> str:
113+
return value.astimezone(timezone.utc).strftime("%Y-%m-%d")
114+
115+
116+
# emit key=value lines to stdout and GitHub Actions output file
117+
def emit_outputs(outputs: dict[str, Any], github_output: str | None) -> None:
118+
lines = [f"{key}={'' if value is None else value}" for key, value in outputs.items()]
119+
for line in lines:
120+
print(line)
121+
if github_output:
122+
with open(github_output, "a", encoding="utf-8") as handle:
123+
for line in lines:
124+
handle.write(f"{line}\n")
125+
126+
127+
# load JSON from file or URL
128+
def load_json(file_path: str | None, url: str | None) -> Any:
129+
if file_path:
130+
text = Path(file_path).read_text(encoding="utf-8")
131+
text = re.sub(r"(?<!:)//[^\n]*", "", text) # strip // line comments, preserve ://
132+
return json.loads(text)
133+
if not url:
134+
raise ValueError("either file_path or url is required")
135+
with urllib.request.urlopen(url, timeout=30) as response:
136+
return json.load(response)
137+
138+
139+
# select latest Gradle release that is at least MIN_DEPENDENCY_AGE_HOURS hours old
140+
def select_gradle_release(args: argparse.Namespace) -> int:
141+
cutoff = now_utc(args.now) - timedelta(hours=args.min_age_hours)
142+
payload = load_json(args.versions_file, args.versions_url)
143+
candidates: list[Candidate] = []
144+
for entry in payload:
145+
version = entry.get("version")
146+
build_time = entry.get("buildTime")
147+
if not version or not build_time:
148+
continue
149+
if any(bool(entry.get(flag)) for flag in ("snapshot", "nightly", "releaseNightly", "broken", "activeRc")):
150+
continue
151+
if entry.get("rcFor") or entry.get("milestoneFor"):
152+
continue
153+
published_at = parse_datetime(build_time)
154+
if published_at <= cutoff:
155+
candidates.append(Candidate(version=version, published_at=published_at))
156+
157+
return emit_selection_result(
158+
label="Gradle",
159+
cutoff=cutoff,
160+
github_output=args.github_output,
161+
candidates=candidates,
162+
not_found_reason=(
163+
f"No eligible stable Gradle release is at least {args.min_age_hours} hours old."
164+
),
165+
current_version=args.current_version,
166+
)
167+
168+
169+
# select latest Maven artifact release that is at least MIN_DEPENDENCY_AGE_HOURS hours old
170+
def select_maven_release(args: argparse.Namespace) -> int:
171+
cutoff = now_utc(args.now) - timedelta(hours=args.min_age_hours)
172+
pattern = combine_patterns(args.prerelease_pattern)
173+
candidates: list[Candidate] = []
174+
for document in load_maven_documents(
175+
group_id=args.group_id,
176+
artifact_id=args.artifact_id,
177+
search_url=args.search_url,
178+
response_file=args.search_response_file,
179+
):
180+
version = document.get("v")
181+
timestamp = document.get("timestamp")
182+
if not version or timestamp is None:
183+
continue
184+
if pattern and pattern.search(version):
185+
continue
186+
published_at = parse_datetime(timestamp)
187+
if published_at <= cutoff:
188+
candidates.append(Candidate(version=version, published_at=published_at))
189+
190+
return emit_selection_result(
191+
label=f"{args.group_id}:{args.artifact_id}",
192+
cutoff=cutoff,
193+
github_output=args.github_output,
194+
candidates=candidates,
195+
not_found_reason=(
196+
f"No eligible stable release found for {args.group_id}:{args.artifact_id} "
197+
f"that is at least {args.min_age_hours} hours old."
198+
),
199+
current_version=args.current_version,
200+
)
201+
202+
203+
# combine prerelease patterns into a single regex pattern
204+
def combine_patterns(patterns: list[str]) -> re.Pattern[str] | None:
205+
non_empty = [pattern for pattern in patterns if pattern]
206+
if not non_empty:
207+
return None
208+
return re.compile("|".join(f"(?:{pattern})" for pattern in non_empty), re.IGNORECASE)
209+
210+
211+
# load all Maven Central versions for given group:artifact
212+
def load_maven_documents(
213+
*,
214+
group_id: str,
215+
artifact_id: str,
216+
search_url: str,
217+
response_file: str | None,
218+
) -> list[dict[str, Any]]:
219+
if response_file:
220+
payload = load_json(response_file, None)
221+
return list(payload.get("response", {}).get("docs", []))
222+
223+
docs: list[dict[str, Any]] = []
224+
start = 0
225+
rows = 200
226+
total = None
227+
while total is None or start < total:
228+
query = urllib.parse.urlencode(
229+
{
230+
"q": f'g:"{group_id}" AND a:"{artifact_id}"',
231+
"core": "gav",
232+
"rows": rows,
233+
"start": start,
234+
"wt": "json",
235+
"sort": "timestamp desc",
236+
}
237+
)
238+
payload = load_json(None, f"{search_url}?{query}")
239+
response = payload.get("response", {})
240+
total = int(response.get("numFound", 0))
241+
batch = list(response.get("docs", []))
242+
docs.extend(batch)
243+
if not batch:
244+
break
245+
start += len(batch)
246+
return docs
247+
248+
249+
# parse a version string into a tuple of ints for numeric comparison (e.g. "3.9.11" → (3, 9, 11))
250+
def _version_sort_key(version: str) -> tuple:
251+
segments = []
252+
for segment in re.split(r"([.\-])", version):
253+
if segment in {"", ".", "-"}:
254+
continue
255+
try:
256+
segments.append((0, int(segment)))
257+
except ValueError:
258+
segments.append((1, segment))
259+
260+
release = []
261+
prerelease = []
262+
for i, seg in enumerate(segments):
263+
if seg[0] == 1: # first string segment starts the prerelease part
264+
prerelease = segments[i:]
265+
break
266+
release.append(seg)
267+
268+
return (tuple(release), not bool(prerelease), tuple(prerelease))
269+
270+
271+
# emit selection result to stdout and GitHub Actions output file for select-gradle and select-maven
272+
def emit_selection_result(
273+
*,
274+
label: str,
275+
cutoff: datetime,
276+
github_output: str | None,
277+
candidates: list[Candidate],
278+
not_found_reason: str,
279+
current_version: str | None = None,
280+
) -> int:
281+
selected = max(candidates, key=lambda candidate: _version_sort_key(candidate.version), default=None)
282+
outputs: dict[str, Any] = {}
283+
284+
# If the current version is already >= the best candidate, keep it
285+
if current_version and (
286+
not selected
287+
or _version_sort_key(current_version) >= _version_sort_key(selected.version)
288+
):
289+
outputs.update(
290+
{
291+
"found": "true",
292+
"version": current_version,
293+
"published_at": "",
294+
"reason": "",
295+
}
296+
)
297+
emit_outputs(outputs, github_output)
298+
if selected:
299+
print(
300+
f"Current version {current_version} for {label} is already >= "
301+
f"latest eligible {selected.version}; keeping current version."
302+
)
303+
else:
304+
print(
305+
f"No eligible version found for {label}; "
306+
f"keeping current version {current_version}."
307+
)
308+
return 0
309+
310+
if not selected:
311+
outputs.update(
312+
{
313+
"found": "false",
314+
"version": "",
315+
"published_at": "",
316+
"reason": not_found_reason,
317+
}
318+
)
319+
emit_outputs(outputs, github_output)
320+
print(f"::error::{not_found_reason}")
321+
return 1
322+
323+
outputs.update(
324+
{
325+
"found": "true",
326+
"version": selected.version,
327+
"published_at": format_date(selected.published_at),
328+
"reason": "",
329+
}
330+
)
331+
emit_outputs(outputs, github_output)
332+
print(
333+
f"Selected latest eligible stable version for {label}: "
334+
f"{selected.version} (published {format_date(selected.published_at)})"
335+
)
336+
return 0
337+
338+
339+
def main() -> int:
340+
args = parse_args()
341+
if args.command == "select-gradle":
342+
return select_gradle_release(args)
343+
if args.command == "select-maven":
344+
return select_maven_release(args)
345+
raise ValueError(f"Unsupported command: {args.command}")
346+
347+
348+
if __name__ == "__main__":
349+
sys.exit(main())

0 commit comments

Comments
 (0)