Auto update Test Matrix for Spring Boot#4743
Conversation
Performance metrics 🚀
|
| 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 |
| 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 |
| # Run every Monday at 9:00 AM UTC | ||
| - cron: '0 9 * * 1' | ||
| workflow_dispatch: # Allow manual triggering | ||
| pull_request: # remove this before merging |
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>
📲 Install BuildsAndroid
|
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 | |||
There was a problem hiding this comment.
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, ordatadog. - 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 | |||
There was a problem hiding this comment.
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, anddatadog— 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
#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
sendDefaultPIIis enabled.🔮 Next steps