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
61 changes: 52 additions & 9 deletions .github/scripts/dependency_age.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def parse_args() -> argparse.Namespace:
def add_common_selection_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--min-age-hours", type=int, default=default_min_age_hours())
parser.add_argument("--now")
parser.add_argument("--current-version", default=None)
parser.add_argument("--github-output", default=None)


Expand Down Expand Up @@ -107,6 +108,11 @@ def format_datetime(value: datetime) -> str:
return value.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


# normalize datetime to YYYY-MM-DD date for more readable PR comment outputs
def format_date(value: datetime) -> str:
return value.astimezone(timezone.utc).strftime("%Y-%m-%d")


# emit key=value lines to stdout and GitHub Actions output file
def emit_outputs(outputs: dict[str, Any], github_output: str | None) -> None:
lines = [f"{key}={'' if value is None else value}" for key, value in outputs.items()]
Expand Down Expand Up @@ -156,6 +162,7 @@ def select_gradle_release(args: argparse.Namespace) -> int:
not_found_reason=(
f"No eligible stable Gradle release is at least {args.min_age_hours} hours old."
),
current_version=args.current_version,
)


Expand Down Expand Up @@ -189,6 +196,7 @@ def select_maven_release(args: argparse.Namespace) -> int:
f"No eligible stable release found for {args.group_id}:{args.artifact_id} "
f"that is at least {args.min_age_hours} hours old."
),
current_version=args.current_version,
)


Expand Down Expand Up @@ -240,15 +248,24 @@ def load_maven_documents(

# parse a version string into a tuple of ints for numeric comparison (e.g. "3.9.11" → (3, 9, 11))
def _version_sort_key(version: str) -> tuple:
parts = []
segments = []
for segment in re.split(r"([.\-])", version):
if segment in {"", ".", "-"}:
continue
try:
parts.append((0, int(segment)))
segments.append((0, int(segment)))
except ValueError:
parts.append((1, segment))
return tuple(parts)
segments.append((1, segment))

release = []
prerelease = []
for i, seg in enumerate(segments):
if seg[0] == 1: # first string segment starts the prerelease part
prerelease = segments[i:]
break
release.append(seg)

return (tuple(release), not bool(prerelease), tuple(prerelease))


# emit selection result to stdout and GitHub Actions output file for select-gradle and select-maven
Expand All @@ -259,11 +276,37 @@ def emit_selection_result(
github_output: str | None,
candidates: list[Candidate],
not_found_reason: str,
current_version: str | None = None,
) -> int:
selected = max(candidates, key=lambda candidate: _version_sort_key(candidate.version), default=None)
outputs: dict[str, Any] = {
"cutoff_at": format_datetime(cutoff),
}
outputs: dict[str, Any] = {}

# If the current version is already >= the best candidate, keep it
if current_version and (
not selected
or _version_sort_key(current_version) >= _version_sort_key(selected.version)
):
Comment on lines +285 to +288
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat stable releases as newer than same-base prereleases

The current_version >= selected.version guard uses _version_sort_key tuple comparison, which ranks a longer tuple as greater when prefixes match; this makes 4.0.0-beta-3 compare as newer than 4.0.0. In the workflow, that causes Maven to stay pinned to a beta even after the stable 4.0.0 becomes eligible, so the updater can get stuck on prereleases instead of moving to GA versions.

Useful? React with 👍 / 👎.

outputs.update(
{
"found": "true",
"version": current_version,
"published_at": "",
"reason": "",
}
)
emit_outputs(outputs, github_output)
if selected:
print(
f"Current version {current_version} for {label} is already >= "
f"latest eligible {selected.version}; keeping current version."
)
else:
print(
f"No eligible version found for {label}; "
f"keeping current version {current_version}."
)
return 0

if not selected:
outputs.update(
{
Expand All @@ -281,14 +324,14 @@ def emit_selection_result(
{
"found": "true",
"version": selected.version,
"published_at": format_datetime(selected.published_at),
"published_at": format_date(selected.published_at),
"reason": "",
}
)
emit_outputs(outputs, github_output)
print(
f"Selected latest eligible stable version for {label}: "
f"{selected.version} (published {format_datetime(selected.published_at)}, cutoff {format_datetime(cutoff)})"
f"{selected.version} (published {format_date(selected.published_at)})"
)
return 0

Expand Down
18 changes: 18 additions & 0 deletions .github/scripts/tests/fixtures/maven-ga-replaces-beta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"response": {
"docs": [
{
"v": "4.0.0",
"timestamp": "2026-04-20T12:00:00Z"
},
{
"v": "4.0.0-beta-3",
"timestamp": "2026-04-18T12:00:00Z"
},
{
"v": "3.9.8",
"timestamp": "2026-04-15T12:00:00Z"
}
]
}
}
104 changes: 100 additions & 4 deletions .github/scripts/tests/test_dependency_age.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
FIXTURES = Path(__file__).resolve().parent / "fixtures"
NOW = "2026-04-24T12:00:00Z"
OUTPUT_PATTERN = re.compile(
r"^(cutoff_at|found|version|published_at|reason)=(.*)$"
r"^(found|version|published_at|reason)=(.*)$"
)


Expand Down Expand Up @@ -48,8 +48,7 @@ def test_selects_previous_gradle_release_when_newest_is_too_new(self) -> None:
self.assertEqual(result.returncode, 0, result.stderr)
outputs = self.parse_outputs(result.stdout)
self.assertEqual(outputs["version"], "9.4.1")
self.assertEqual(outputs["published_at"], "2026-04-22T11:00:00Z")
self.assertEqual(outputs["cutoff_at"], "2026-04-22T12:00:00Z")
self.assertEqual(outputs["published_at"], "2026-04-22")

def test_reports_when_no_eligible_gradle_release_exists(self) -> None:
result = self.run_script(
Expand Down Expand Up @@ -121,7 +120,104 @@ def test_exact_48_hour_boundary_is_accepted(self) -> None:
self.assertEqual(result.returncode, 0, result.stderr)
outputs = self.parse_outputs(result.stdout)
self.assertEqual(outputs["version"], "3.5.5")
self.assertEqual(outputs["published_at"], "2026-04-22T12:00:00Z")
self.assertEqual(outputs["published_at"], "2026-04-22")


def test_ga_version_overrides_current_prerelease(self) -> None:
result = self.run_script(
"select-maven",
"--now",
NOW,
"--group-id",
"org.apache.maven",
"--artifact-id",
"apache-maven",
"--search-response-file",
str(FIXTURES / "maven-ga-replaces-beta.json"),
"--prerelease-pattern",
"alpha",
"--prerelease-pattern",
"beta",
"--prerelease-pattern",
"rc",
"--current-version",
"4.0.0-beta-3",
)

self.assertEqual(result.returncode, 0, result.stderr)
outputs = self.parse_outputs(result.stdout)
self.assertEqual(outputs["found"], "true")
self.assertEqual(outputs["version"], "4.0.0")
self.assertEqual(outputs["published_at"], "2026-04-20")

def test_keeps_current_version_when_higher_than_eligible(self) -> None:
result = self.run_script(
"select-maven",
"--now",
NOW,
"--group-id",
"org.apache.maven",
"--artifact-id",
"apache-maven",
"--search-response-file",
str(FIXTURES / "maven-newest-too-new.json"),
"--prerelease-pattern",
"alpha",
"--prerelease-pattern",
"beta",
"--prerelease-pattern",
"rc",
"--current-version",
"4.0.0-beta-3",
)

self.assertEqual(result.returncode, 0, result.stderr)
outputs = self.parse_outputs(result.stdout)
self.assertEqual(outputs["found"], "true")
self.assertEqual(outputs["version"], "4.0.0-beta-3")
self.assertEqual(outputs["published_at"], "")

def test_updates_when_eligible_version_is_higher_than_current(self) -> None:
result = self.run_script(
"select-maven",
"--now",
NOW,
"--group-id",
"org.apache.maven.plugins",
"--artifact-id",
"maven-surefire-plugin",
"--search-response-file",
str(FIXTURES / "surefire-boundary.json"),
"--prerelease-pattern",
"alpha",
"--prerelease-pattern",
"beta",
"--current-version",
"3.5.4",
)

self.assertEqual(result.returncode, 0, result.stderr)
outputs = self.parse_outputs(result.stdout)
self.assertEqual(outputs["found"], "true")
self.assertEqual(outputs["version"], "3.5.5")
self.assertEqual(outputs["published_at"], "2026-04-22")

def test_keeps_current_version_when_no_eligible_version_exists(self) -> None:
result = self.run_script(
"select-gradle",
"--now",
NOW,
"--versions-file",
str(FIXTURES / "gradle-no-eligible.json"),
"--current-version",
"9.0.0",
)

self.assertEqual(result.returncode, 0, result.stderr)
outputs = self.parse_outputs(result.stdout)
self.assertEqual(outputs["found"], "true")
self.assertEqual(outputs["version"], "9.0.0")
self.assertEqual(outputs["published_at"], "")


if __name__ == "__main__":
Expand Down
37 changes: 29 additions & 8 deletions .github/workflows/update-smoke-test-latest-versions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,22 @@ jobs:
DATE=$(date +'%Y%m%d')
echo "branch=ci/update-smoke-test-latest-versions-${DATE}" >> "$GITHUB_OUTPUT"

- name: Read current pinned versions
id: current
run: |
gradle_props="dd-smoke-tests/gradle/src/test/resources/latest-tool-versions.properties"
maven_props="dd-smoke-tests/maven/src/test/resources/latest-tool-versions.properties"
get_prop() { grep "^$1=" "$2" 2>/dev/null | cut -d= -f2 || true; }
echo "gradle_version=$(get_prop gradle.version "$gradle_props")" >> "$GITHUB_OUTPUT"
echo "maven_version=$(get_prop maven.version "$maven_props")" >> "$GITHUB_OUTPUT"
echo "surefire_version=$(get_prop maven-surefire.version "$maven_props")" >> "$GITHUB_OUTPUT"

- name: Resolve latest eligible Gradle version
id: gradle
run: |
python3 .github/scripts/dependency_age.py select-gradle \
--min-age-hours "${MIN_DEPENDENCY_AGE_HOURS}" \
--current-version "${{ steps.current.outputs.gradle_version }}" \
--github-output "$GITHUB_OUTPUT"

- name: Resolve latest eligible stable Maven version
Expand All @@ -46,6 +57,7 @@ jobs:
--prerelease-pattern beta \
--prerelease-pattern rc \
--min-age-hours "${MIN_DEPENDENCY_AGE_HOURS}" \
--current-version "${{ steps.current.outputs.maven_version }}" \
--github-output "$GITHUB_OUTPUT"

- name: Resolve latest eligible stable Maven Surefire version
Expand All @@ -57,19 +69,29 @@ jobs:
--prerelease-pattern alpha \
--prerelease-pattern beta \
--min-age-hours "${MIN_DEPENDENCY_AGE_HOURS}" \
--current-version "${{ steps.current.outputs.surefire_version }}" \
--github-output "$GITHUB_OUTPUT"

- name: Update properties files
id: update
env:
GRADLE_VERSION: ${{ steps.gradle.outputs.version }}
GRADLE_PUBLISHED: ${{ steps.gradle.outputs.published_at }}
MAVEN_VERSION: ${{ steps.maven.outputs.version }}
MAVEN_PUBLISHED: ${{ steps.maven.outputs.published_at }}
SUREFIRE_VERSION: ${{ steps.surefire.outputs.version }}
SUREFIRE_PUBLISHED: ${{ steps.surefire.outputs.published_at }}
run: |
version_line() { if [ -n "$2" ]; then echo "$1 (published $2)"; else echo "$1 (unchanged)"; fi; }
echo "Writing latest eligible stable versions (>=${MIN_DEPENDENCY_AGE_HOURS}h old) to properties files:"
echo " Gradle: ${GRADLE_VERSION} (published ${{ steps.gradle.outputs.published_at }})"
echo " Maven: ${MAVEN_VERSION} (published ${{ steps.maven.outputs.published_at }})"
echo " Maven Surefire: ${SUREFIRE_VERSION} (published ${{ steps.surefire.outputs.published_at }})"
echo " Eligibility cutoff: ${{ steps.gradle.outputs.cutoff_at }}"
echo " Gradle: $(version_line "${GRADLE_VERSION}" "${GRADLE_PUBLISHED}")"
echo " Maven: $(version_line "${MAVEN_VERSION}" "${MAVEN_PUBLISHED}")"
echo " Maven Surefire: $(version_line "${SUREFIRE_VERSION}" "${SUREFIRE_PUBLISHED}")"

# Build version lines for PR body
echo "gradle_line=$(version_line "${GRADLE_VERSION}" "${GRADLE_PUBLISHED}")" >> "$GITHUB_OUTPUT"
echo "maven_line=$(version_line "${MAVEN_VERSION}" "${MAVEN_PUBLISHED}")" >> "$GITHUB_OUTPUT"
echo "surefire_line=$(version_line "${SUREFIRE_VERSION}" "${SUREFIRE_PUBLISHED}")" >> "$GITHUB_OUTPUT"

printf '%s\n' \
"# Pinned latest eligible stable versions (>=${MIN_DEPENDENCY_AGE_HOURS}h old) for CI Visibility Gradle smoke tests." \
Expand Down Expand Up @@ -138,10 +160,9 @@ jobs:
This PR updates the pinned latest eligible stable tool versions used by CI Visibility smoke tests.
Only releases at least ${{ env.MIN_DEPENDENCY_AGE_HOURS }} hours old are eligible.

- Gradle: ${{ steps.gradle.outputs.version }} (published ${{ steps.gradle.outputs.published_at }})
- Maven: ${{ steps.maven.outputs.version }} (published ${{ steps.maven.outputs.published_at }})
- Maven Surefire: ${{ steps.surefire.outputs.version }} (published ${{ steps.surefire.outputs.published_at }})
- Eligibility cutoff: ${{ steps.gradle.outputs.cutoff_at }}
- Gradle: ${{ steps.update.outputs.gradle_line }}
- Maven: ${{ steps.update.outputs.maven_line }}
- Maven Surefire: ${{ steps.update.outputs.surefire_line }}

# Motivation

Expand Down