Skip to content

Auto update Test Matrix for Spring Boot#4743

Draft
adinauer wants to merge 20 commits into
mainfrom
feat/spring-boot-matrix-auto-update
Draft

Auto update Test Matrix for Spring Boot#4743
adinauer wants to merge 20 commits into
mainfrom
feat/spring-boot-matrix-auto-update

Conversation

@adinauer

@adinauer adinauer commented Sep 22, 2025

Copy link
Copy Markdown
Member

#skip-changelog

📜 Description

Auto update Test Matrix for Spring Boot

💡 Motivation and Context

So we can test against newly released versions automatically and not rely on customers reporting issues.

💚 How did you test it?

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

@github-actions

github-actions Bot commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 324.49 ms 367.94 ms 43.45 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
18c0bc2 306.73 ms 349.77 ms 43.03 ms
0eaac1e 316.82 ms 357.34 ms 40.52 ms
d15471f 303.49 ms 439.08 ms 135.59 ms
fc5ccaf 276.52 ms 370.46 ms 93.93 ms
e2dce0b 308.96 ms 360.10 ms 51.14 ms
5b1a06b 352.27 ms 413.70 ms 61.43 ms
37ec571 366.04 ms 424.28 ms 58.23 ms
9fbb112 361.43 ms 427.57 ms 66.14 ms
bbc35bb 324.88 ms 425.73 ms 100.85 ms
ff8eea4 313.42 ms 337.08 ms 23.66 ms

App size

Revision Plain With Sentry Diff
18c0bc2 1.58 MiB 2.13 MiB 557.33 KiB
0eaac1e 1.58 MiB 2.19 MiB 619.17 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
fc5ccaf 1.58 MiB 2.13 MiB 557.54 KiB
e2dce0b 0 B 0 B 0 B
5b1a06b 0 B 0 B 0 B
37ec571 0 B 0 B 0 B
9fbb112 1.58 MiB 2.11 MiB 539.18 KiB
bbc35bb 1.58 MiB 2.12 MiB 553.01 KiB
ff8eea4 1.58 MiB 2.28 MiB 718.64 KiB

Previous results on branch: feat/spring-boot-matrix-auto-update

Startup times

Revision Plain With Sentry Diff
2fcca66 358.71 ms 432.55 ms 73.84 ms

App size

Revision Plain With Sentry Diff
2fcca66 0 B 0 B 0 B

Comment on lines +35 to +316
cat << 'EOF' > update_versions.py
import json
import os
import re
import requests
from packaging import version
import sys
from pathlib import Path

def get_spring_boot_versions():
"""Fetch all Spring Boot versions from Maven Central with retry logic"""

max_retries = 3
timeout = 60

for attempt in range(max_retries):
try:
print(f"Fetching versions (attempt {attempt + 1}/{max_retries})...")

# Try the Maven Central REST API first
rest_url = "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot/maven-metadata.xml"
response = requests.get(rest_url, timeout=timeout)

if response.status_code == 200:
print("Using Maven metadata XML approach...")
# Parse XML to extract versions
import xml.etree.ElementTree as ET
root = ET.fromstring(response.text)
versions = []
versioning = root.find('versioning')
if versioning is not None:
versions_element = versioning.find('versions')
if versions_element is not None:
for version_elem in versions_element.findall('version'):
v = version_elem.text
if v and not any(suffix in v for suffix in ['SNAPSHOT', 'RC', 'BUILD', 'RELEASE']):
# Only include versions that start with a digit and use standard format
if v and v[0].isdigit() and v.count('.') >= 2:
versions.append(v)

if versions:
print(f"Found {len(versions)} versions via XML")
print(f"Sample versions: {versions[-10:] if len(versions) > 10 else versions}")
# Filter out any versions that still can't be parsed
valid_versions = []
for v in versions:
try:
version.parse(v)
valid_versions.append(v)
except Exception as e:
print(f"Skipping invalid version format: {v}")
print(f"Filtered to {len(valid_versions)} valid versions")
return sorted(valid_versions, key=version.parse)

# Fallback to search API
print("Trying search API fallback...")
search_url = "https://search.maven.org/solrsearch/select"
params = {
"q": "g:\"org.springframework.boot\" AND a:\"spring-boot\"",
"core": "gav",
"rows": 1000,
"wt": "json"
}

response = requests.get(search_url, params=params, timeout=timeout)
response.raise_for_status()
data = response.json()

if 'response' not in data or 'docs' not in data['response']:
raise Exception(f"Unexpected API response structure")

docs = data['response']['docs']
print(f"Found {len(docs)} documents in search response")

if docs and len(docs) > 0:
print(f"Sample doc structure: {list(docs[0].keys())}")

versions = []
for doc in docs:
version_field = doc.get('v') or doc.get('version')
if (version_field and
not any(suffix in version_field for suffix in ['SNAPSHOT', 'RC', 'BUILD', 'RELEASE']) and
version_field[0].isdigit() and version_field.count('.') >= 2):
versions.append(version_field)

if versions:
# Filter out any versions that still can't be parsed
valid_versions = []
for v in versions:
try:
version.parse(v)
valid_versions.append(v)
except Exception as e:
print(f"Skipping invalid version format: {v}")
print(f"Successfully fetched {len(valid_versions)} valid versions via search API")
return sorted(valid_versions, key=version.parse)

except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
print("Retrying...")
continue

print("All attempts failed")
return []

def parse_current_versions(json_file):
"""Parse current Spring Boot versions from JSON data file"""
if not Path(json_file).exists():
return []

try:
with open(json_file, 'r') as f:
data = json.load(f)
return data.get('versions', [])
except Exception as e:
print(f"Error reading {json_file}: {e}")
return []

def get_latest_patch(all_versions, minor_version):
"""Get the latest patch version for a given minor version"""
target_minor = '.'.join(minor_version.split('.')[:2])
patches = [v for v in all_versions if v.startswith(target_minor + '.')]
return max(patches, key=version.parse) if patches else minor_version

def update_version_matrix(current_versions, all_versions, major_version):
"""Update version matrix based on available versions"""
if not current_versions or not all_versions:
return current_versions, False

# Filter versions for this major version
major_versions = [v for v in all_versions if v.startswith(f"{major_version}.")]
if not major_versions:
return current_versions, False

updated_versions = []
changes_made = False

# Always keep the minimum supported version (first version)
min_version = current_versions[0]
updated_versions.append(min_version)

# Update patch versions for existing minor versions
for curr_version in current_versions[1:]: # Skip min version
if any(suffix in curr_version for suffix in ['M', 'RC', 'SNAPSHOT']):
# Keep milestone/RC versions as-is for pre-release majors
updated_versions.append(curr_version)
continue

latest_patch = get_latest_patch(major_versions, curr_version)
if latest_patch != curr_version:
print(f"Updating {curr_version} -> {latest_patch}")
changes_made = True
updated_versions.append(latest_patch)

# Check for new minor versions
current_minors = set()
for v in current_versions:
if not any(suffix in v for suffix in ['M', 'RC', 'SNAPSHOT']):
current_minors.add('.'.join(v.split('.')[:2]))

available_minors = set()
for v in major_versions:
if not any(suffix in v for suffix in ['M', 'RC', 'SNAPSHOT']):
available_minors.add('.'.join(v.split('.')[:2]))

new_minors = available_minors - current_minors
if new_minors:
# Add latest patch of new minor versions
for new_minor in sorted(new_minors, key=version.parse):
latest_patch = get_latest_patch(major_versions, new_minor + '.0')
updated_versions.append(latest_patch)
print(f"Adding new minor version: {latest_patch}")
changes_made = True

# Remove second oldest minor (but keep absolute minimum)
if len(updated_versions) > 7: # If we have more than 7 versions
# Sort by version, keep min version and remove second oldest
sorted_versions = sorted(updated_versions, key=version.parse)
min_version = sorted_versions[0]
other_versions = sorted_versions[1:]

# Keep all but the oldest of the "other" versions
if len(other_versions) > 6:
updated_versions = [min_version] + other_versions[1:]
print(f"Removed second oldest version: {other_versions[0]}")
changes_made = True

# Sort final versions and remove duplicates
min_version = updated_versions[0]
other_versions = sorted([v for v in updated_versions if v != min_version], key=version.parse)
final_versions = [min_version] + other_versions

# Remove duplicates while preserving order
seen = set()
deduplicated_versions = []
for v in final_versions:
if v not in seen:
seen.add(v)
deduplicated_versions.append(v)

if len(deduplicated_versions) != len(final_versions):
print(f"Removed {len(final_versions) - len(deduplicated_versions)} duplicate versions")

return deduplicated_versions, changes_made

def update_json_file(json_file, new_versions):
"""Update the JSON data file with new versions"""
try:
# Write new versions to JSON file with consistent formatting
data = {"versions": new_versions}
with open(json_file, 'w') as f:
json.dump(data, f, indent=2, separators=(',', ': '))
f.write('\n') # Add trailing newline
return True
except Exception as e:
print(f"Error writing to {json_file}: {e}")
return False

def main():
print("Fetching Spring Boot versions...")
all_versions = get_spring_boot_versions()

if not all_versions:
print("No versions found, exiting")
sys.exit(1)

print(f"Found {len(all_versions)} versions")

data_files = [
(".github/data/spring-boot-2-versions.json", "2"),
(".github/data/spring-boot-3-versions.json", "3"),
(".github/data/spring-boot-4-versions.json", "4")
]

changes_made = False
change_summary = []

for json_file, major_version in data_files:
if not Path(json_file).exists():
continue

print(f"\nProcessing {json_file} (Spring Boot {major_version}.x)")

current_versions = parse_current_versions(json_file)
if not current_versions:
continue

print(f"Current versions: {current_versions}")

new_versions, file_changed = update_version_matrix(current_versions, all_versions, major_version)

if file_changed:
print(f"New versions: {new_versions}")
if update_json_file(json_file, new_versions):
changes_made = True
change_summary.append(f"Spring Boot {major_version}.x: {' -> '.join([str(current_versions), str(new_versions)])}")
else:
print("No changes needed")

if changes_made:
print(f"\nChanges made to Spring Boot version files:")
for change in change_summary:
print(f" - {change}")

# Write summary for GitHub output
with open('version_changes.txt', 'w') as f:
f.write('\n'.join(change_summary))

# Set GitHub output for use in PR description
with open(os.environ.get('GITHUB_OUTPUT', '/dev/null'), 'a') as f:
f.write(f"changes_summary<<EOF\n")
f.write('\n'.join(change_summary))
f.write(f"\nEOF\n")
else:
print("\nNo version updates needed")

sys.exit(0 if changes_made else 1)

if __name__ == "__main__":
main()
EOF

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we move this to /scripts?

# Run every Monday at 9:00 AM UTC
- cron: '0 9 * * 1'
workflow_dispatch: # Allow manual triggering
pull_request: # remove this before merging

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

remove this before merging

Base automatically changed from feat/spring-boot-test-matrix to main September 26, 2025 11:29
adinauer and others added 2 commits May 28, 2026 11:03
Move the Spring Boot version update logic out of the workflow and into a reusable script. Keep the workflow responsible for installing dependencies and invoking the script.

Co-Authored-By: Claude <noreply@anthropic.com>
@sentry

sentry Bot commented May 28, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.45.0 (1) release

⚙️ sentry-android Build Distribution Settings

Comment thread .github/workflows/spring-boot-4-matrix.yml Outdated
adinauer and others added 3 commits June 25, 2026 15:52
Pin action references used by the Spring Boot matrix and version update workflows. Remove the temporary pull request trigger now that the workflow is ready to run on schedule or manually.

Co-Authored-By: Claude <noreply@anthropic.com>
Pass matrix versions through environment variables before using them in shell scripts. Validate each value against the expected major version pattern to avoid executing malformed matrix data.

Co-Authored-By: Claude <noreply@anthropic.com>
@@ -0,0 +1,223 @@
# Memory

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Vendored file missing required attribution header fields

org/datadog/jmxfetch/default-jmx-metrics.yaml — This file is vendored from Datadog's jmxfetch project (the directory path org/datadog/jmxfetch/ is Datadog's own package namespace) but has no attribution header. Add a comment block at the top with all four required fields: vendoring origin phrase (e.g., "Vendored from DataDog/jmxfetch"), copyright holder, license name, and source URL (e.g., https://github.com/DataDog/jmxfetch).

Evidence
  • The file resides under org/datadog/jmxfetch/, the Java package namespace of DataDog's open-source jmxfetch project (https://github.com/DataDog/jmxfetch).
  • Lines 1–223 of the new file contain no Copyright, Licensed under, SPDX-License-Identifier, or vendoring-origin phrase.
  • THIRD_PARTY_NOTICES.md has zero matches for jmxfetch, DataDog, or datadog.
  • All four required header fields (vendoring origin, copyright, license name, source URL) are absent.

Identified by Warden check-code-attribution · CZJ-R3N

@@ -0,0 +1,223 @@
# Memory

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Missing THIRD_PARTY_NOTICES.md entry for vendored Datadog jmxfetch file

org/datadog/jmxfetch/default-jmx-metrics.yaml — This vendored Datadog jmxfetch configuration file has no entry in THIRD_PARTY_NOTICES.md. Add an entry with Source URL (https://github.com/DataDog/jmxfetch), License name, Copyright, Scope (this file path), and the full license text.

Evidence
  • Searched THIRD_PARTY_NOTICES.md for jmxfetch, DataDog, and datadog — no matches found.
  • The file is new (all-addition diff) and placed under org/datadog/jmxfetch/, clearly identifying it as third-party Datadog code.
  • A THIRD_PARTY_NOTICES.md entry is mandatory for all vendored files regardless of header completeness.

Identified by Warden check-code-attribution · 3PD-GN8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants