Skip to content

Commit 1acfdf8

Browse files
authored
Merge 261a569 into c1d64e7
2 parents c1d64e7 + 261a569 commit 1acfdf8

File tree

1 file changed

+326
-0
lines changed

1 file changed

+326
-0
lines changed
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
name: Update Spring Boot Versions
2+
3+
on:
4+
schedule:
5+
# Run every Monday at 9:00 AM UTC
6+
- cron: '0 9 * * 1'
7+
workflow_dispatch: # Allow manual triggering
8+
pull_request: # remove this before merging
9+
10+
permissions:
11+
contents: write
12+
pull-requests: write
13+
14+
jobs:
15+
update-spring-boot-versions:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v5
20+
with:
21+
token: ${{ secrets.GITHUB_TOKEN }}
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v6
25+
with:
26+
python-version: '3.11'
27+
28+
- name: Install dependencies
29+
run: |
30+
pip install requests packaging
31+
32+
- name: Update Spring Boot versions
33+
run: |
34+
cat << 'EOF' > update_versions.py
35+
import json
36+
import re
37+
import requests
38+
from packaging import version
39+
import sys
40+
from pathlib import Path
41+
42+
def get_spring_boot_versions():
43+
"""Fetch all Spring Boot versions from Maven Central with retry logic"""
44+
# Use the versions API instead of the general search
45+
url = "https://search.maven.org/solrsearch/select"
46+
params = {
47+
"q": "g:org.springframework.boot AND a:spring-boot",
48+
"core": "gav",
49+
"rows": 500,
50+
"wt": "json"
51+
}
52+
53+
max_retries = 3
54+
timeout = 60 # Increased timeout
55+
56+
for attempt in range(max_retries):
57+
try:
58+
print(f"Fetching versions (attempt {attempt + 1}/{max_retries})...")
59+
response = requests.get(url, params=params, timeout=timeout)
60+
response.raise_for_status()
61+
data = response.json()
62+
63+
if 'response' not in data or 'docs' not in data['response']:
64+
raise Exception(f"Unexpected API response structure: {data}")
65+
66+
docs = data['response']['docs']
67+
print(f"Found {len(docs)} documents in response")
68+
69+
if docs and len(docs) > 0:
70+
print(f"Sample doc structure: {list(docs[0].keys())}")
71+
72+
versions = []
73+
for doc in docs:
74+
# Try different possible version field names
75+
version_field = None
76+
if 'v' in doc:
77+
version_field = doc['v']
78+
elif 'version' in doc:
79+
version_field = doc['version']
80+
elif 'latestVersion' in doc:
81+
# This might be a summary doc, skip it
82+
print(f"Skipping summary doc with latestVersion: {doc.get('latestVersion')}")
83+
continue
84+
else:
85+
print(f"Warning: No version field found in doc: {doc}")
86+
continue
87+
88+
# Only include release versions (no SNAPSHOT, RC, M versions for 2.x and 3.x)
89+
if not any(suffix in version_field for suffix in ['SNAPSHOT', 'RC', 'BUILD']):
90+
versions.append(version_field)
91+
92+
print(f"Successfully fetched {len(versions)} versions")
93+
return sorted(versions, key=version.parse)
94+
except requests.exceptions.Timeout as e:
95+
print(f"Attempt {attempt + 1} timed out: {e}")
96+
if attempt < max_retries - 1:
97+
print("Retrying...")
98+
continue
99+
except Exception as e:
100+
print(f"Attempt {attempt + 1} failed: {e}")
101+
if attempt < max_retries - 1:
102+
print("Retrying...")
103+
continue
104+
105+
print("All attempts failed")
106+
return []
107+
108+
def parse_current_versions(workflow_file):
109+
"""Parse current Spring Boot versions from workflow file"""
110+
content = Path(workflow_file).read_text()
111+
112+
# Find the springboot-version matrix line
113+
pattern = r'springboot-version:\s*\[\s*([^\]]+)\s*\]'
114+
match = re.search(pattern, content)
115+
116+
if not match:
117+
return []
118+
119+
# Extract versions from the match
120+
versions_str = match.group(1)
121+
versions = []
122+
for v in versions_str.split(','):
123+
v = v.strip().strip("'\"")
124+
if v:
125+
versions.append(v)
126+
127+
return versions
128+
129+
def get_latest_patch(all_versions, minor_version):
130+
"""Get the latest patch version for a given minor version"""
131+
target_minor = '.'.join(minor_version.split('.')[:2])
132+
patches = [v for v in all_versions if v.startswith(target_minor + '.')]
133+
return max(patches, key=version.parse) if patches else minor_version
134+
135+
def update_version_matrix(current_versions, all_versions, major_version):
136+
"""Update version matrix based on available versions"""
137+
if not current_versions or not all_versions:
138+
return current_versions, False
139+
140+
# Filter versions for this major version
141+
major_versions = [v for v in all_versions if v.startswith(f"{major_version}.")]
142+
if not major_versions:
143+
return current_versions, False
144+
145+
updated_versions = []
146+
changes_made = False
147+
148+
# Always keep the minimum supported version (first version)
149+
min_version = current_versions[0]
150+
updated_versions.append(min_version)
151+
152+
# Update patch versions for existing minor versions
153+
for curr_version in current_versions[1:]: # Skip min version
154+
if any(suffix in curr_version for suffix in ['M', 'RC', 'SNAPSHOT']):
155+
# Keep milestone/RC versions as-is for pre-release majors
156+
updated_versions.append(curr_version)
157+
continue
158+
159+
latest_patch = get_latest_patch(major_versions, curr_version)
160+
if latest_patch != curr_version:
161+
print(f"Updating {curr_version} -> {latest_patch}")
162+
changes_made = True
163+
updated_versions.append(latest_patch)
164+
165+
# Check for new minor versions
166+
current_minors = set()
167+
for v in current_versions:
168+
if not any(suffix in v for suffix in ['M', 'RC', 'SNAPSHOT']):
169+
current_minors.add('.'.join(v.split('.')[:2]))
170+
171+
available_minors = set()
172+
for v in major_versions:
173+
if not any(suffix in v for suffix in ['M', 'RC', 'SNAPSHOT']):
174+
available_minors.add('.'.join(v.split('.')[:2]))
175+
176+
new_minors = available_minors - current_minors
177+
if new_minors:
178+
# Add latest patch of new minor versions
179+
for new_minor in sorted(new_minors, key=version.parse):
180+
latest_patch = get_latest_patch(major_versions, new_minor + '.0')
181+
updated_versions.append(latest_patch)
182+
print(f"Adding new minor version: {latest_patch}")
183+
changes_made = True
184+
185+
# Remove second oldest minor (but keep absolute minimum)
186+
if len(updated_versions) > 7: # If we have more than 7 versions
187+
# Sort by version, keep min version and remove second oldest
188+
sorted_versions = sorted(updated_versions, key=version.parse)
189+
min_version = sorted_versions[0]
190+
other_versions = sorted_versions[1:]
191+
192+
# Keep all but the oldest of the "other" versions
193+
if len(other_versions) > 6:
194+
updated_versions = [min_version] + other_versions[1:]
195+
print(f"Removed second oldest version: {other_versions[0]}")
196+
changes_made = True
197+
198+
# Sort final versions
199+
min_version = updated_versions[0]
200+
other_versions = sorted([v for v in updated_versions if v != min_version], key=version.parse)
201+
final_versions = [min_version] + other_versions
202+
203+
return final_versions, changes_made
204+
205+
def update_workflow_file(workflow_file, new_versions):
206+
"""Update the workflow file with new versions"""
207+
content = Path(workflow_file).read_text()
208+
209+
# Format new versions for YAML
210+
versions_str = ", ".join([f"'{v}'" for v in new_versions])
211+
new_matrix_line = f" springboot-version: [ {versions_str} ]"
212+
213+
# Replace the matrix line
214+
pattern = r'(\s*)springboot-version:\s*\[\s*[^\]]+\s*\]'
215+
replacement = new_matrix_line
216+
217+
updated_content = re.sub(pattern, replacement, content)
218+
219+
if updated_content != content:
220+
Path(workflow_file).write_text(updated_content)
221+
return True
222+
return False
223+
224+
def main():
225+
print("Fetching Spring Boot versions...")
226+
all_versions = get_spring_boot_versions()
227+
228+
if not all_versions:
229+
print("No versions found, exiting")
230+
sys.exit(1)
231+
232+
print(f"Found {len(all_versions)} versions")
233+
234+
workflows = [
235+
(".github/workflows/spring-boot-2-matrix.yml", "2"),
236+
(".github/workflows/spring-boot-3-matrix.yml", "3"),
237+
(".github/workflows/spring-boot-4-matrix.yml", "4")
238+
]
239+
240+
changes_made = False
241+
change_summary = []
242+
243+
for workflow_file, major_version in workflows:
244+
if not Path(workflow_file).exists():
245+
continue
246+
247+
print(f"\nProcessing {workflow_file} (Spring Boot {major_version}.x)")
248+
249+
current_versions = parse_current_versions(workflow_file)
250+
if not current_versions:
251+
continue
252+
253+
print(f"Current versions: {current_versions}")
254+
255+
new_versions, file_changed = update_version_matrix(current_versions, all_versions, major_version)
256+
257+
if file_changed:
258+
print(f"New versions: {new_versions}")
259+
if update_workflow_file(workflow_file, new_versions):
260+
changes_made = True
261+
change_summary.append(f"Spring Boot {major_version}.x: {' -> '.join([str(current_versions), str(new_versions)])}")
262+
else:
263+
print("No changes needed")
264+
265+
if changes_made:
266+
print(f"\nChanges made to workflows:")
267+
for change in change_summary:
268+
print(f" - {change}")
269+
270+
# Write summary for later use
271+
with open('version_changes.txt', 'w') as f:
272+
f.write('\n'.join(change_summary))
273+
else:
274+
print("\nNo version updates needed")
275+
276+
sys.exit(0 if changes_made else 1)
277+
278+
if __name__ == "__main__":
279+
main()
280+
EOF
281+
282+
python update_versions.py
283+
284+
- name: Check for changes
285+
id: changes
286+
run: |
287+
if git diff --quiet; then
288+
echo "has_changes=false" >> $GITHUB_OUTPUT
289+
else
290+
echo "has_changes=true" >> $GITHUB_OUTPUT
291+
fi
292+
293+
- name: Create Pull Request
294+
if: steps.changes.outputs.has_changes == 'true'
295+
uses: peter-evans/create-pull-request@v7
296+
with:
297+
token: ${{ secrets.GITHUB_TOKEN }}
298+
base: feat/spring-boot-matrix-auto-update
299+
commit-message: "chore: Update Spring Boot version matrices"
300+
title: "Automated Spring Boot Version Update"
301+
body: |
302+
## Automated Spring Boot Version Update
303+
304+
This PR updates the Spring Boot version matrices in our test workflows based on the latest available versions.
305+
306+
### Changes Made:
307+
$(cat version_changes.txt 2>/dev/null || echo "See diff for changes")
308+
309+
### Update Strategy:
310+
- **Patch updates**: Updated to latest patch version of existing minor versions
311+
- **New minor versions**: Added new minor versions and removed second oldest (keeping minimum supported)
312+
- **Minimum version preserved**: Always keeps the minimum supported version for compatibility testing
313+
314+
This ensures our CI tests stay current with Spring Boot releases while maintaining coverage of older versions that users may still be using.
315+
branch: automated-spring-boot-version-update
316+
delete-branch: true
317+
draft: false
318+
319+
- name: Summary
320+
run: |
321+
if [ "${{ steps.changes.outputs.has_changes }}" = "true" ]; then
322+
echo "✅ Spring Boot version updates found and PR created"
323+
cat version_changes.txt 2>/dev/null || true
324+
else
325+
echo "ℹ️ No Spring Boot version updates needed"
326+
fi

0 commit comments

Comments
 (0)