Skip to content

Commit 8309e2e

Browse files
adinauerclaude
andcommitted
ci: Extract Spring Boot version updater script
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>
1 parent ede9293 commit 8309e2e

2 files changed

Lines changed: 285 additions & 289 deletions

File tree

.github/workflows/update-spring-boot-versions.yml

Lines changed: 1 addition & 289 deletions
Original file line numberDiff line numberDiff line change
@@ -31,295 +31,7 @@ jobs:
3131
3232
- name: Update Spring Boot versions
3333
id: update_versions
34-
run: |
35-
cat << 'EOF' > update_versions.py
36-
import json
37-
import os
38-
import re
39-
import requests
40-
from packaging import version
41-
import sys
42-
from pathlib import Path
43-
44-
def get_spring_boot_versions():
45-
"""Fetch all Spring Boot versions from Maven Central with retry logic"""
46-
47-
max_retries = 3
48-
timeout = 60
49-
50-
for attempt in range(max_retries):
51-
try:
52-
print(f"Fetching versions (attempt {attempt + 1}/{max_retries})...")
53-
54-
# Try the Maven Central REST API first
55-
rest_url = "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot/maven-metadata.xml"
56-
response = requests.get(rest_url, timeout=timeout)
57-
58-
if response.status_code == 200:
59-
print("Using Maven metadata XML approach...")
60-
# Parse XML to extract versions
61-
import xml.etree.ElementTree as ET
62-
root = ET.fromstring(response.text)
63-
versions = []
64-
versioning = root.find('versioning')
65-
if versioning is not None:
66-
versions_element = versioning.find('versions')
67-
if versions_element is not None:
68-
for version_elem in versions_element.findall('version'):
69-
v = version_elem.text
70-
if v and not any(suffix in v for suffix in ['SNAPSHOT', 'RC', 'BUILD', 'RELEASE']):
71-
# Only include versions that start with a digit and use standard format
72-
if v and v[0].isdigit() and v.count('.') >= 2:
73-
versions.append(v)
74-
75-
if versions:
76-
print(f"Found {len(versions)} versions via XML")
77-
print(f"Sample versions: {versions[-10:] if len(versions) > 10 else versions}")
78-
# Filter out any versions that still can't be parsed
79-
valid_versions = []
80-
for v in versions:
81-
try:
82-
version.parse(v)
83-
valid_versions.append(v)
84-
except Exception as e:
85-
print(f"Skipping invalid version format: {v}")
86-
print(f"Filtered to {len(valid_versions)} valid versions")
87-
return sorted(valid_versions, key=version.parse)
88-
89-
# Fallback to search API
90-
print("Trying search API fallback...")
91-
search_url = "https://search.maven.org/solrsearch/select"
92-
params = {
93-
"q": "g:\"org.springframework.boot\" AND a:\"spring-boot\"",
94-
"core": "gav",
95-
"rows": 1000,
96-
"wt": "json"
97-
}
98-
99-
response = requests.get(search_url, params=params, timeout=timeout)
100-
response.raise_for_status()
101-
data = response.json()
102-
103-
if 'response' not in data or 'docs' not in data['response']:
104-
raise Exception(f"Unexpected API response structure")
105-
106-
docs = data['response']['docs']
107-
print(f"Found {len(docs)} documents in search response")
108-
109-
if docs and len(docs) > 0:
110-
print(f"Sample doc structure: {list(docs[0].keys())}")
111-
112-
versions = []
113-
for doc in docs:
114-
version_field = doc.get('v') or doc.get('version')
115-
if (version_field and
116-
not any(suffix in version_field for suffix in ['SNAPSHOT', 'RC', 'BUILD', 'RELEASE']) and
117-
version_field[0].isdigit() and version_field.count('.') >= 2):
118-
versions.append(version_field)
119-
120-
if versions:
121-
# Filter out any versions that still can't be parsed
122-
valid_versions = []
123-
for v in versions:
124-
try:
125-
version.parse(v)
126-
valid_versions.append(v)
127-
except Exception as e:
128-
print(f"Skipping invalid version format: {v}")
129-
print(f"Successfully fetched {len(valid_versions)} valid versions via search API")
130-
return sorted(valid_versions, key=version.parse)
131-
132-
except Exception as e:
133-
print(f"Attempt {attempt + 1} failed: {e}")
134-
if attempt < max_retries - 1:
135-
print("Retrying...")
136-
continue
137-
138-
print("All attempts failed")
139-
return []
140-
141-
def parse_current_versions(json_file):
142-
"""Parse current Spring Boot versions from JSON data file"""
143-
if not Path(json_file).exists():
144-
return []
145-
146-
try:
147-
with open(json_file, 'r') as f:
148-
data = json.load(f)
149-
return data.get('versions', [])
150-
except Exception as e:
151-
print(f"Error reading {json_file}: {e}")
152-
return []
153-
154-
def get_latest_patch(all_versions, minor_version):
155-
"""Get the latest patch version for a given minor version"""
156-
target_minor = '.'.join(minor_version.split('.')[:2])
157-
patches = [v for v in all_versions if v.startswith(target_minor + '.')]
158-
return max(patches, key=version.parse) if patches else minor_version
159-
160-
def update_version_matrix(current_versions, all_versions, major_version):
161-
"""Update version matrix based on available versions"""
162-
if not current_versions or not all_versions:
163-
return current_versions, False
164-
165-
# Filter versions for this major version
166-
major_versions = [v for v in all_versions if v.startswith(f"{major_version}.")]
167-
if not major_versions:
168-
return current_versions, False
169-
170-
updated_versions = []
171-
changes_made = False
172-
173-
# Always keep the minimum supported version (first version)
174-
min_version = current_versions[0]
175-
updated_versions.append(min_version)
176-
177-
# Update patch versions for existing minor versions
178-
for curr_version in current_versions[1:]: # Skip min version
179-
if any(suffix in curr_version for suffix in ['M', 'RC', 'SNAPSHOT']):
180-
# Keep milestone/RC versions as-is for pre-release majors
181-
updated_versions.append(curr_version)
182-
continue
183-
184-
latest_patch = get_latest_patch(major_versions, curr_version)
185-
if latest_patch != curr_version:
186-
print(f"Updating {curr_version} -> {latest_patch}")
187-
changes_made = True
188-
updated_versions.append(latest_patch)
189-
190-
# Check for new minor versions
191-
current_minors = set()
192-
for v in current_versions:
193-
if not any(suffix in v for suffix in ['M', 'RC', 'SNAPSHOT']):
194-
current_minors.add('.'.join(v.split('.')[:2]))
195-
196-
available_minors = set()
197-
for v in major_versions:
198-
if not any(suffix in v for suffix in ['M', 'RC', 'SNAPSHOT']):
199-
available_minors.add('.'.join(v.split('.')[:2]))
200-
201-
new_minors = available_minors - current_minors
202-
if new_minors:
203-
# Add latest patch of new minor versions
204-
for new_minor in sorted(new_minors, key=version.parse):
205-
latest_patch = get_latest_patch(major_versions, new_minor + '.0')
206-
updated_versions.append(latest_patch)
207-
print(f"Adding new minor version: {latest_patch}")
208-
changes_made = True
209-
210-
# Remove second oldest minor (but keep absolute minimum)
211-
if len(updated_versions) > 7: # If we have more than 7 versions
212-
# Sort by version, keep min version and remove second oldest
213-
sorted_versions = sorted(updated_versions, key=version.parse)
214-
min_version = sorted_versions[0]
215-
other_versions = sorted_versions[1:]
216-
217-
# Keep all but the oldest of the "other" versions
218-
if len(other_versions) > 6:
219-
updated_versions = [min_version] + other_versions[1:]
220-
print(f"Removed second oldest version: {other_versions[0]}")
221-
changes_made = True
222-
223-
# Sort final versions and remove duplicates
224-
min_version = updated_versions[0]
225-
other_versions = sorted([v for v in updated_versions if v != min_version], key=version.parse)
226-
final_versions = [min_version] + other_versions
227-
228-
# Remove duplicates while preserving order
229-
seen = set()
230-
deduplicated_versions = []
231-
for v in final_versions:
232-
if v not in seen:
233-
seen.add(v)
234-
deduplicated_versions.append(v)
235-
236-
if len(deduplicated_versions) != len(final_versions):
237-
print(f"Removed {len(final_versions) - len(deduplicated_versions)} duplicate versions")
238-
239-
return deduplicated_versions, changes_made
240-
241-
def update_json_file(json_file, new_versions):
242-
"""Update the JSON data file with new versions"""
243-
try:
244-
# Write new versions to JSON file with consistent formatting
245-
data = {"versions": new_versions}
246-
with open(json_file, 'w') as f:
247-
json.dump(data, f, indent=2, separators=(',', ': '))
248-
f.write('\n') # Add trailing newline
249-
return True
250-
except Exception as e:
251-
print(f"Error writing to {json_file}: {e}")
252-
return False
253-
254-
def main():
255-
print("Fetching Spring Boot versions...")
256-
all_versions = get_spring_boot_versions()
257-
258-
if not all_versions:
259-
print("No versions found, exiting")
260-
sys.exit(1)
261-
262-
print(f"Found {len(all_versions)} versions")
263-
264-
data_files = [
265-
(".github/data/spring-boot-2-versions.json", "2"),
266-
(".github/data/spring-boot-3-versions.json", "3"),
267-
(".github/data/spring-boot-4-versions.json", "4")
268-
]
269-
270-
changes_made = False
271-
change_summary = []
272-
273-
for json_file, major_version in data_files:
274-
if not Path(json_file).exists():
275-
continue
276-
277-
print(f"\nProcessing {json_file} (Spring Boot {major_version}.x)")
278-
279-
current_versions = parse_current_versions(json_file)
280-
if not current_versions:
281-
continue
282-
283-
print(f"Current versions: {current_versions}")
284-
285-
new_versions, file_changed = update_version_matrix(current_versions, all_versions, major_version)
286-
287-
if file_changed:
288-
print(f"New versions: {new_versions}")
289-
if update_json_file(json_file, new_versions):
290-
changes_made = True
291-
change_summary.append(f"Spring Boot {major_version}.x: {' -> '.join([str(current_versions), str(new_versions)])}")
292-
else:
293-
print("No changes needed")
294-
295-
if changes_made:
296-
print(f"\nChanges made to Spring Boot version files:")
297-
for change in change_summary:
298-
print(f" - {change}")
299-
300-
# Write summary for GitHub output
301-
with open('version_changes.txt', 'w') as f:
302-
f.write('\n'.join(change_summary))
303-
304-
# Set GitHub output for use in PR description
305-
with open(os.environ.get('GITHUB_OUTPUT', '/dev/null'), 'a') as f:
306-
f.write(f"changes_summary<<EOF\n")
307-
f.write('\n'.join(change_summary))
308-
f.write(f"\nEOF\n")
309-
else:
310-
print("\nNo version updates needed")
311-
312-
sys.exit(0 if changes_made else 1)
313-
314-
if __name__ == "__main__":
315-
main()
316-
EOF
317-
318-
python update_versions.py
319-
320-
- name: Clean up temporary files
321-
run: |
322-
rm -f update_versions.py version_changes.txt
34+
run: python scripts/update-spring-boot-versions.py
32335

32436
- name: Check for changes
32537
id: changes

0 commit comments

Comments
 (0)