From c1d64e71bb7a05862dcf37c440bd357691b7a11f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 19 Sep 2025 14:56:22 +0200 Subject: [PATCH 01/15] Create Test Matrix for Spring Boot --- .github/workflows/spring-boot-2-matrix.yml | 143 +++++++++++++++++++ .github/workflows/spring-boot-3-matrix.yml | 151 +++++++++++++++++++++ .github/workflows/spring-boot-4-matrix.yml | 144 ++++++++++++++++++++ 3 files changed, 438 insertions(+) create mode 100644 .github/workflows/spring-boot-2-matrix.yml create mode 100644 .github/workflows/spring-boot-3-matrix.yml create mode 100644 .github/workflows/spring-boot-4-matrix.yml diff --git a/.github/workflows/spring-boot-2-matrix.yml b/.github/workflows/spring-boot-2-matrix.yml new file mode 100644 index 0000000000..81235d4906 --- /dev/null +++ b/.github/workflows/spring-boot-2-matrix.yml @@ -0,0 +1,143 @@ +name: Spring Boot 2.x Matrix + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + spring-boot-2-matrix: + timeout-minutes: 45 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + springboot-version: [ '2.1.0', '2.2.5', '2.4.13', '2.5.15', '2.6.15', '2.7.0', '2.7.18' ] + + name: Spring Boot ${{ matrix.springboot-version }} + env: + SENTRY_URL: http://127.0.0.1:8000 + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + steps: + - name: Checkout Repo + uses: actions/checkout@v5 + with: + submodules: 'recursive' + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.10.5' + + - name: Install Python dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements.txt + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + - name: Update Spring Boot 2.x version + run: | + sed -i 's/^springboot2=.*/springboot2=${{ matrix.springboot-version }}/' gradle/libs.versions.toml + echo "Updated Spring Boot 2.x version to ${{ matrix.springboot-version }}" + + - name: Exclude android modules from build + run: | + sed -i \ + -e '/.*"sentry-android-ndk",/d' \ + -e '/.*"sentry-android",/d' \ + -e '/.*"sentry-compose",/d' \ + -e '/.*"sentry-android-core",/d' \ + -e '/.*"sentry-android-fragment",/d' \ + -e '/.*"sentry-android-navigation",/d' \ + -e '/.*"sentry-android-sqlite",/d' \ + -e '/.*"sentry-android-timber",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \ + -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \ + -e '/.*"sentry-samples:sentry-samples-android",/d' \ + -e '/.*"sentry-android-replay",/d' \ + settings.gradle.kts + + - name: Exclude android modules from ignore list + run: | + sed -i \ + -e '/.*"sentry-uitest-android",/d' \ + -e '/.*"sentry-uitest-android-benchmark",/d' \ + -e '/.*"sentry-uitest-android-critical",/d' \ + -e '/.*"test-app-sentry",/d' \ + -e '/.*"sentry-samples-android",/d' \ + build.gradle.kts + + - name: Build SDK + run: | + ./gradlew assemble --parallel + + - name: Test sentry-samples-spring-boot + run: | + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot" \ + --agent false \ + --auto-init "true" \ + --build "true" + + - name: Test sentry-samples-spring-boot-webflux + run: | + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-webflux" \ + --agent false \ + --auto-init "true" \ + --build "true" + + - name: Test sentry-samples-spring-boot-opentelemetry + run: | + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-opentelemetry" \ + --agent true \ + --auto-init "true" \ + --build "true" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-springboot-2-${{ matrix.springboot-version }} + path: | + **/build/reports/* + **/build/test-results/**/*.xml + sentry-mock-server.txt + spring-server.txt + + - name: Test Report + uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15 + if: always() + with: + name: JUnit Spring Boot 2.x ${{ matrix.springboot-version }} + path: | + **/build/test-results/**/*.xml + reporter: java-junit + output-to: step-summary + fail-on-error: false + + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: '**/build/test-results/**/*.xml' diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml new file mode 100644 index 0000000000..99968f3ae8 --- /dev/null +++ b/.github/workflows/spring-boot-3-matrix.yml @@ -0,0 +1,151 @@ +name: Spring Boot 3.x Matrix + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + spring-boot-3-matrix: + timeout-minutes: 45 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + springboot-version: [ '3.0.0', '3.2.10', '3.3.5', '3.4.5', '3.5.6' ] + + name: Spring Boot ${{ matrix.springboot-version }} + env: + SENTRY_URL: http://127.0.0.1:8000 + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + steps: + - name: Checkout Repo + uses: actions/checkout@v5 + with: + submodules: 'recursive' + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.10.5' + + - name: Install Python dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements.txt + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + - name: Update Spring Boot 3.x version + run: | + sed -i 's/^springboot3=.*/springboot3=${{ matrix.springboot-version }}/' gradle/libs.versions.toml + echo "Updated Spring Boot 3.x version to ${{ matrix.springboot-version }}" + + - name: Exclude android modules from build + run: | + sed -i \ + -e '/.*"sentry-android-ndk",/d' \ + -e '/.*"sentry-android",/d' \ + -e '/.*"sentry-compose",/d' \ + -e '/.*"sentry-android-core",/d' \ + -e '/.*"sentry-android-fragment",/d' \ + -e '/.*"sentry-android-navigation",/d' \ + -e '/.*"sentry-android-sqlite",/d' \ + -e '/.*"sentry-android-timber",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \ + -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \ + -e '/.*"sentry-samples:sentry-samples-android",/d' \ + -e '/.*"sentry-android-replay",/d' \ + settings.gradle.kts + + - name: Exclude android modules from ignore list + run: | + sed -i \ + -e '/.*"sentry-uitest-android",/d' \ + -e '/.*"sentry-uitest-android-benchmark",/d' \ + -e '/.*"sentry-uitest-android-critical",/d' \ + -e '/.*"test-app-sentry",/d' \ + -e '/.*"sentry-samples-android",/d' \ + build.gradle.kts + + - name: Build SDK + run: | + ./gradlew assemble --parallel + + - name: Test sentry-samples-spring-boot-jakarta + run: | + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-jakarta" \ + --agent false \ + --auto-init "true" \ + --build "true" + + - name: Test sentry-samples-spring-boot-webflux-jakarta + run: | + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-webflux-jakarta" \ + --agent false \ + --auto-init "true" \ + --build "true" + + - name: Test sentry-samples-spring-boot-jakarta-opentelemetry + run: | + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-jakarta-opentelemetry" \ + --agent true \ + --auto-init "true" \ + --build "true" + + - name: Test sentry-samples-spring-boot-jakarta-opentelemetry-noagent + run: | + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" \ + --agent false \ + --auto-init "true" \ + --build "true" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-springboot-3-${{ matrix.springboot-version }} + path: | + **/build/reports/* + **/build/test-results/**/*.xml + sentry-mock-server.txt + spring-server.txt + + - name: Test Report + uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15 + if: always() + with: + name: JUnit Spring Boot 3.x ${{ matrix.springboot-version }} + path: | + **/build/test-results/**/*.xml + reporter: java-junit + output-to: step-summary + fail-on-error: false + + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: '**/build/test-results/**/*.xml' diff --git a/.github/workflows/spring-boot-4-matrix.yml b/.github/workflows/spring-boot-4-matrix.yml new file mode 100644 index 0000000000..63ed063f23 --- /dev/null +++ b/.github/workflows/spring-boot-4-matrix.yml @@ -0,0 +1,144 @@ +name: Spring Boot 4.x Matrix + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + spring-boot-4-matrix: + timeout-minutes: 45 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + springboot-version: [ '4.0.0-M1', '4.0.0-M2', '4.0.0-M3' ] + + name: Spring Boot ${{ matrix.springboot-version }} + env: + SENTRY_URL: http://127.0.0.1:8000 + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + steps: + - name: Checkout Repo + uses: actions/checkout@v5 + with: + submodules: 'recursive' + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.10.5' + + - name: Install Python dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements.txt + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + + - name: Update Spring Boot 4.x version + run: | + sed -i 's/^springboot4=.*/springboot4=${{ matrix.springboot-version }}/' gradle/libs.versions.toml + echo "Updated Spring Boot 4.x version to ${{ matrix.springboot-version }}" + + - name: Exclude android modules from build + run: | + sed -i \ + -e '/.*"sentry-android-ndk",/d' \ + -e '/.*"sentry-android",/d' \ + -e '/.*"sentry-compose",/d' \ + -e '/.*"sentry-android-core",/d' \ + -e '/.*"sentry-android-fragment",/d' \ + -e '/.*"sentry-android-navigation",/d' \ + -e '/.*"sentry-android-sqlite",/d' \ + -e '/.*"sentry-android-timber",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \ + -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \ + -e '/.*"sentry-samples:sentry-samples-android",/d' \ + -e '/.*"sentry-android-replay",/d' \ + settings.gradle.kts + + - name: Exclude android modules from ignore list + run: | + sed -i \ + -e '/.*"sentry-uitest-android",/d' \ + -e '/.*"sentry-uitest-android-benchmark",/d' \ + -e '/.*"sentry-uitest-android-critical",/d' \ + -e '/.*"test-app-sentry",/d' \ + -e '/.*"sentry-samples-android",/d' \ + build.gradle.kts + + - name: Build SDK + run: | + ./gradlew assemble --parallel + + - name: Run Spring Boot 4.x system tests + run: | + # Test standard Spring Boot 4 modules + echo "Testing sentry-samples-spring-boot-4 (standard)" + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-4" \ + --agent false \ + --auto-init "true" \ + --build "true" + + echo "Testing sentry-samples-spring-boot-4-webflux (standard)" + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-4-webflux" \ + --agent false \ + --auto-init "true" \ + --build "true" + + # Test OpenTelemetry modules + echo "Testing sentry-samples-spring-boot-4-opentelemetry (with agent)" + python3 test/system-test-runner.py test \ + --module "sentry-samples-spring-boot-4-opentelemetry" \ + --agent true \ + --auto-init "true" \ + --build "true" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-springboot-4-${{ matrix.springboot-version }} + path: | + **/build/reports/* + **/build/test-results/**/*.xml + sentry-mock-server.txt + spring-server.txt + + - name: Test Report + uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15 + if: always() + with: + name: JUnit Spring Boot 4.x ${{ matrix.springboot-version }} + path: | + **/build/test-results/**/*.xml + reporter: java-junit + output-to: step-summary + fail-on-error: false + + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: '**/build/test-results/**/*.xml' From 3325b0ce7d491698acd6348de12d563357f470ff Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 13:50:30 +0200 Subject: [PATCH 02/15] auto update spring boot versions for matrix test --- .../workflows/update-spring-boot-versions.yml | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 .github/workflows/update-spring-boot-versions.yml diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml new file mode 100644 index 0000000000..9ad90fba78 --- /dev/null +++ b/.github/workflows/update-spring-boot-versions.yml @@ -0,0 +1,285 @@ +name: Update Spring Boot Versions + +on: + schedule: + # Run every Monday at 9:00 AM UTC + - cron: '0 9 * * 1' + workflow_dispatch: # Allow manual triggering + +permissions: + contents: write + pull-requests: write + +jobs: + update-spring-boot-versions: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install requests packaging + + - name: Update Spring Boot versions + run: | + cat << 'EOF' > update_versions.py + import json + import re + import requests + from packaging import version + import yaml + import sys + from pathlib import Path + + def get_spring_boot_versions(): + """Fetch all Spring Boot versions from Maven Central""" + url = "https://search.maven.org/solrsearch/select" + params = { + "q": "g:org.springframework.boot AND a:spring-boot", + "rows": 200, + "wt": "json" + } + + try: + response = requests.get(url, params=params, timeout=30) + response.raise_for_status() + data = response.json() + + versions = [] + for doc in data['response']['docs']: + v = doc['v'] + # Only include release versions (no SNAPSHOT, RC, M versions for 2.x and 3.x) + if not any(suffix in v for suffix in ['SNAPSHOT', 'RC', 'BUILD']): + versions.append(v) + + return sorted(versions, key=version.parse) + except Exception as e: + print(f"Error fetching versions: {e}") + return [] + + def parse_current_versions(workflow_file): + """Parse current Spring Boot versions from workflow file""" + content = Path(workflow_file).read_text() + + # Find the springboot-version matrix line + pattern = r'springboot-version:\s*\[\s*([^\]]+)\s*\]' + match = re.search(pattern, content) + + if not match: + return [] + + # Extract versions from the match + versions_str = match.group(1) + versions = [] + for v in versions_str.split(','): + v = v.strip().strip("'\"") + if v: + versions.append(v) + + return versions + + 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 + 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 + + return final_versions, changes_made + + def update_workflow_file(workflow_file, new_versions): + """Update the workflow file with new versions""" + content = Path(workflow_file).read_text() + + # Format new versions for YAML + versions_str = ", ".join([f"'{v}'" for v in new_versions]) + new_matrix_line = f" springboot-version: [ {versions_str} ]" + + # Replace the matrix line + pattern = r'(\s*)springboot-version:\s*\[\s*[^\]]+\s*\]' + replacement = new_matrix_line + + updated_content = re.sub(pattern, replacement, content) + + if updated_content != content: + Path(workflow_file).write_text(updated_content) + return True + 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") + + workflows = [ + (".github/workflows/spring-boot-2-matrix.yml", "2"), + (".github/workflows/spring-boot-3-matrix.yml", "3"), + (".github/workflows/spring-boot-4-matrix.yml", "4") + ] + + changes_made = False + change_summary = [] + + for workflow_file, major_version in workflows: + if not Path(workflow_file).exists(): + continue + + print(f"\nProcessing {workflow_file} (Spring Boot {major_version}.x)") + + current_versions = parse_current_versions(workflow_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_workflow_file(workflow_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 workflows:") + for change in change_summary: + print(f" - {change}") + + # Write summary for later use + with open('version_changes.txt', 'w') as f: + f.write('\n'.join(change_summary)) + else: + print("\nNo version updates needed") + + sys.exit(0 if changes_made else 1) + + if __name__ == "__main__": + main() + EOF + + python update_versions.py + + - name: Check for changes + id: changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "chore: Update Spring Boot version matrices" + title: "Automated Spring Boot Version Update" + body: | + ## Automated Spring Boot Version Update + + This PR updates the Spring Boot version matrices in our test workflows based on the latest available versions. + + ### Changes Made: + $(cat version_changes.txt 2>/dev/null || echo "See diff for changes") + + ### Update Strategy: + - **Patch updates**: Updated to latest patch version of existing minor versions + - **New minor versions**: Added new minor versions and removed second oldest (keeping minimum supported) + - **Minimum version preserved**: Always keeps the minimum supported version for compatibility testing + + This ensures our CI tests stay current with Spring Boot releases while maintaining coverage of older versions that users may still be using. + branch: automated-spring-boot-version-update + delete-branch: true + draft: false + + - name: Summary + run: | + if [ "${{ steps.changes.outputs.has_changes }}" = "true" ]; then + echo "✅ Spring Boot version updates found and PR created" + cat version_changes.txt 2>/dev/null || true + else + echo "ℹ️ No Spring Boot version updates needed" + fi From 3eb7e3ed9274bc7453488e63f6d6be9a76efdaf1 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 14:17:33 +0200 Subject: [PATCH 03/15] trigger on GH --- .github/workflows/update-spring-boot-versions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 9ad90fba78..e97223de2e 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -5,6 +5,7 @@ on: # Run every Monday at 9:00 AM UTC - cron: '0 9 * * 1' workflow_dispatch: # Allow manual triggering + pull_request: # remove this before merging permissions: contents: write From 4d98eb5d6112fa705df89077c02fdb27f946f7f0 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 14:27:23 +0200 Subject: [PATCH 04/15] fix error --- .../workflows/update-spring-boot-versions.yml | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index e97223de2e..38a1632dd4 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -36,7 +36,6 @@ jobs: import re import requests from packaging import version - import yaml import sys from pathlib import Path @@ -48,19 +47,19 @@ jobs: "rows": 200, "wt": "json" } - + try: response = requests.get(url, params=params, timeout=30) response.raise_for_status() data = response.json() - + versions = [] for doc in data['response']['docs']: v = doc['v'] # Only include release versions (no SNAPSHOT, RC, M versions for 2.x and 3.x) if not any(suffix in v for suffix in ['SNAPSHOT', 'RC', 'BUILD']): versions.append(v) - + return sorted(versions, key=version.parse) except Exception as e: print(f"Error fetching versions: {e}") @@ -69,14 +68,14 @@ jobs: def parse_current_versions(workflow_file): """Parse current Spring Boot versions from workflow file""" content = Path(workflow_file).read_text() - + # Find the springboot-version matrix line pattern = r'springboot-version:\s*\[\s*([^\]]+)\s*\]' match = re.search(pattern, content) - + if not match: return [] - + # Extract versions from the match versions_str = match.group(1) versions = [] @@ -84,7 +83,7 @@ jobs: v = v.strip().strip("'\"") if v: versions.append(v) - + return versions def get_latest_patch(all_versions, minor_version): @@ -97,43 +96,43 @@ jobs: """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 @@ -142,41 +141,41 @@ jobs: 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 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 - + return final_versions, changes_made def update_workflow_file(workflow_file, new_versions): """Update the workflow file with new versions""" content = Path(workflow_file).read_text() - + # Format new versions for YAML versions_str = ", ".join([f"'{v}'" for v in new_versions]) new_matrix_line = f" springboot-version: [ {versions_str} ]" - + # Replace the matrix line pattern = r'(\s*)springboot-version:\s*\[\s*[^\]]+\s*\]' replacement = new_matrix_line - + updated_content = re.sub(pattern, replacement, content) - + if updated_content != content: Path(workflow_file).write_text(updated_content) return True @@ -185,36 +184,36 @@ jobs: 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") - + workflows = [ (".github/workflows/spring-boot-2-matrix.yml", "2"), (".github/workflows/spring-boot-3-matrix.yml", "3"), (".github/workflows/spring-boot-4-matrix.yml", "4") ] - + changes_made = False change_summary = [] - + for workflow_file, major_version in workflows: if not Path(workflow_file).exists(): continue - + print(f"\nProcessing {workflow_file} (Spring Boot {major_version}.x)") - + current_versions = parse_current_versions(workflow_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_workflow_file(workflow_file, new_versions): @@ -222,18 +221,18 @@ jobs: 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 workflows:") for change in change_summary: print(f" - {change}") - + # Write summary for later use with open('version_changes.txt', 'w') as f: f.write('\n'.join(change_summary)) else: print("\nNo version updates needed") - + sys.exit(0 if changes_made else 1) if __name__ == "__main__": @@ -260,17 +259,17 @@ jobs: title: "Automated Spring Boot Version Update" body: | ## Automated Spring Boot Version Update - + This PR updates the Spring Boot version matrices in our test workflows based on the latest available versions. - + ### Changes Made: $(cat version_changes.txt 2>/dev/null || echo "See diff for changes") - + ### Update Strategy: - **Patch updates**: Updated to latest patch version of existing minor versions - **New minor versions**: Added new minor versions and removed second oldest (keeping minimum supported) - **Minimum version preserved**: Always keeps the minimum supported version for compatibility testing - + This ensures our CI tests stay current with Spring Boot releases while maintaining coverage of older versions that users may still be using. branch: automated-spring-boot-version-update delete-branch: true From 3ac36dc579c40171778fba1e650607ee562b7bbc Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 14:35:31 +0200 Subject: [PATCH 05/15] retry logic and higher timeout --- .../workflows/update-spring-boot-versions.yml | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 38a1632dd4..9df5ad8cd9 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -40,7 +40,7 @@ jobs: from pathlib import Path def get_spring_boot_versions(): - """Fetch all Spring Boot versions from Maven Central""" + """Fetch all Spring Boot versions from Maven Central with retry logic""" url = "https://search.maven.org/solrsearch/select" params = { "q": "g:org.springframework.boot AND a:spring-boot", @@ -48,22 +48,38 @@ jobs: "wt": "json" } - try: - response = requests.get(url, params=params, timeout=30) - response.raise_for_status() - data = response.json() - - versions = [] - for doc in data['response']['docs']: - v = doc['v'] - # Only include release versions (no SNAPSHOT, RC, M versions for 2.x and 3.x) - if not any(suffix in v for suffix in ['SNAPSHOT', 'RC', 'BUILD']): - versions.append(v) - - return sorted(versions, key=version.parse) - except Exception as e: - print(f"Error fetching versions: {e}") - return [] + max_retries = 3 + timeout = 60 # Increased timeout + + for attempt in range(max_retries): + try: + print(f"Fetching versions (attempt {attempt + 1}/{max_retries})...") + response = requests.get(url, params=params, timeout=timeout) + response.raise_for_status() + data = response.json() + + versions = [] + for doc in data['response']['docs']: + v = doc['v'] + # Only include release versions (no SNAPSHOT, RC, M versions for 2.x and 3.x) + if not any(suffix in v for suffix in ['SNAPSHOT', 'RC', 'BUILD']): + versions.append(v) + + print(f"Successfully fetched {len(versions)} versions") + return sorted(versions, key=version.parse) + except requests.exceptions.Timeout as e: + print(f"Attempt {attempt + 1} timed out: {e}") + if attempt < max_retries - 1: + print("Retrying...") + continue + 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(workflow_file): """Parse current Spring Boot versions from workflow file""" From 9f4685a5953c47accc03ee55d83c0fc28c018656 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 14:59:30 +0200 Subject: [PATCH 06/15] debug --- .github/workflows/update-spring-boot-versions.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 9df5ad8cd9..b34461a4d2 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -58,8 +58,20 @@ jobs: 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: {data}") + + docs = data['response']['docs'] + print(f"Found {len(docs)} documents in response") + + if docs and len(docs) > 0: + print(f"Sample doc structure: {list(docs[0].keys())}") + versions = [] - for doc in data['response']['docs']: + for doc in docs: + if 'v' not in doc: + print(f"Warning: 'v' field missing from doc: {doc}") + continue v = doc['v'] # Only include release versions (no SNAPSHOT, RC, M versions for 2.x and 3.x) if not any(suffix in v for suffix in ['SNAPSHOT', 'RC', 'BUILD']): From 53a038a1195a761a474282b0e15ad8d833eea46f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 15:04:30 +0200 Subject: [PATCH 07/15] fix --- .../workflows/update-spring-boot-versions.yml | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index b34461a4d2..3cb19ae09b 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -41,10 +41,12 @@ jobs: def get_spring_boot_versions(): """Fetch all Spring Boot versions from Maven Central with retry logic""" + # Use the versions API instead of the general search url = "https://search.maven.org/solrsearch/select" params = { "q": "g:org.springframework.boot AND a:spring-boot", - "rows": 200, + "core": "gav", + "rows": 500, "wt": "json" } @@ -69,13 +71,23 @@ jobs: versions = [] for doc in docs: - if 'v' not in doc: - print(f"Warning: 'v' field missing from doc: {doc}") + # Try different possible version field names + version_field = None + if 'v' in doc: + version_field = doc['v'] + elif 'version' in doc: + version_field = doc['version'] + elif 'latestVersion' in doc: + # This might be a summary doc, skip it + print(f"Skipping summary doc with latestVersion: {doc.get('latestVersion')}") continue - v = doc['v'] + else: + print(f"Warning: No version field found in doc: {doc}") + continue + # Only include release versions (no SNAPSHOT, RC, M versions for 2.x and 3.x) - if not any(suffix in v for suffix in ['SNAPSHOT', 'RC', 'BUILD']): - versions.append(v) + if not any(suffix in version_field for suffix in ['SNAPSHOT', 'RC', 'BUILD']): + versions.append(version_field) print(f"Successfully fetched {len(versions)} versions") return sorted(versions, key=version.parse) From b58b374c39abca49b244e9f35084bcb48768e544 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 15:19:39 +0200 Subject: [PATCH 08/15] specify base --- .github/workflows/update-spring-boot-versions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 3cb19ae09b..2a461d8115 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -295,6 +295,7 @@ jobs: uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} + base: main commit-message: "chore: Update Spring Boot version matrices" title: "Automated Spring Boot Version Update" body: | From 261a5691af7e855169c16cb901ac8e067bbcf10d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 15:22:40 +0200 Subject: [PATCH 09/15] temporarily use branch as base --- .github/workflows/update-spring-boot-versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 2a461d8115..5c41adfff6 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -295,7 +295,7 @@ jobs: uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} - base: main + base: feat/spring-boot-matrix-auto-update commit-message: "chore: Update Spring Boot version matrices" title: "Automated Spring Boot Version Update" body: | From 10288b359797207e2d50965debea13ad1e86c5b2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 15:49:28 +0200 Subject: [PATCH 10/15] specify spring boot versions in separate file so it can be updated --- .github/data/spring-boot-2-versions.json | 3 + .github/data/spring-boot-3-versions.json | 3 + .github/data/spring-boot-4-versions.json | 3 + .github/workflows/spring-boot-2-matrix.yml | 14 +++- .github/workflows/spring-boot-3-matrix.yml | 14 +++- .github/workflows/spring-boot-4-matrix.yml | 14 +++- .../workflows/update-spring-boot-versions.yml | 73 ++++++++----------- 7 files changed, 77 insertions(+), 47 deletions(-) create mode 100644 .github/data/spring-boot-2-versions.json create mode 100644 .github/data/spring-boot-3-versions.json create mode 100644 .github/data/spring-boot-4-versions.json diff --git a/.github/data/spring-boot-2-versions.json b/.github/data/spring-boot-2-versions.json new file mode 100644 index 0000000000..03917b60cd --- /dev/null +++ b/.github/data/spring-boot-2-versions.json @@ -0,0 +1,3 @@ +{ + "versions": ["2.1.0", "2.2.5", "2.4.13", "2.5.15", "2.6.15", "2.7.0", "2.7.18"] +} diff --git a/.github/data/spring-boot-3-versions.json b/.github/data/spring-boot-3-versions.json new file mode 100644 index 0000000000..558edf79f7 --- /dev/null +++ b/.github/data/spring-boot-3-versions.json @@ -0,0 +1,3 @@ +{ + "versions": ["3.0.0", "3.2.10", "3.3.5", "3.4.5", "3.5.6"] +} diff --git a/.github/data/spring-boot-4-versions.json b/.github/data/spring-boot-4-versions.json new file mode 100644 index 0000000000..4a0c62e5c7 --- /dev/null +++ b/.github/data/spring-boot-4-versions.json @@ -0,0 +1,3 @@ +{ + "versions": ["4.0.0-M1", "4.0.0-M2", "4.0.0-M3"] +} diff --git a/.github/workflows/spring-boot-2-matrix.yml b/.github/workflows/spring-boot-2-matrix.yml index 81235d4906..2651549463 100644 --- a/.github/workflows/spring-boot-2-matrix.yml +++ b/.github/workflows/spring-boot-2-matrix.yml @@ -11,13 +11,25 @@ concurrency: cancel-in-progress: true jobs: + load-versions: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Checkout Repo + uses: actions/checkout@v5 + - name: Set matrix data + id: set-matrix + run: echo "matrix=$(cat .github/data/spring-boot-2-versions.json | jq -c .versions)" >> $GITHUB_OUTPUT + spring-boot-2-matrix: + needs: load-versions timeout-minutes: 45 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - springboot-version: [ '2.1.0', '2.2.5', '2.4.13', '2.5.15', '2.6.15', '2.7.0', '2.7.18' ] + springboot-version: ${{ fromJSON(needs.load-versions.outputs.matrix) }} name: Spring Boot ${{ matrix.springboot-version }} env: diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml index 99968f3ae8..cf5a0813f4 100644 --- a/.github/workflows/spring-boot-3-matrix.yml +++ b/.github/workflows/spring-boot-3-matrix.yml @@ -11,13 +11,25 @@ concurrency: cancel-in-progress: true jobs: + load-versions: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Checkout Repo + uses: actions/checkout@v5 + - name: Set matrix data + id: set-matrix + run: echo "matrix=$(cat .github/data/spring-boot-3-versions.json | jq -c .versions)" >> $GITHUB_OUTPUT + spring-boot-3-matrix: + needs: load-versions timeout-minutes: 45 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - springboot-version: [ '3.0.0', '3.2.10', '3.3.5', '3.4.5', '3.5.6' ] + springboot-version: ${{ fromJSON(needs.load-versions.outputs.matrix) }} name: Spring Boot ${{ matrix.springboot-version }} env: diff --git a/.github/workflows/spring-boot-4-matrix.yml b/.github/workflows/spring-boot-4-matrix.yml index 63ed063f23..351497eda0 100644 --- a/.github/workflows/spring-boot-4-matrix.yml +++ b/.github/workflows/spring-boot-4-matrix.yml @@ -11,13 +11,25 @@ concurrency: cancel-in-progress: true jobs: + load-versions: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Checkout Repo + uses: actions/checkout@v5 + - name: Set matrix data + id: set-matrix + run: echo "matrix=$(cat .github/data/spring-boot-4-versions.json | jq -c .versions)" >> $GITHUB_OUTPUT + spring-boot-4-matrix: + needs: load-versions timeout-minutes: 45 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - springboot-version: [ '4.0.0-M1', '4.0.0-M2', '4.0.0-M3' ] + springboot-version: ${{ fromJSON(needs.load-versions.outputs.matrix) }} name: Spring Boot ${{ matrix.springboot-version }} env: diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 5c41adfff6..5ba3bc0c47 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -105,26 +105,18 @@ jobs: print("All attempts failed") return [] - def parse_current_versions(workflow_file): - """Parse current Spring Boot versions from workflow file""" - content = Path(workflow_file).read_text() - - # Find the springboot-version matrix line - pattern = r'springboot-version:\s*\[\s*([^\]]+)\s*\]' - match = re.search(pattern, content) - - if not match: + def parse_current_versions(json_file): + """Parse current Spring Boot versions from JSON data file""" + if not Path(json_file).exists(): return [] - # Extract versions from the match - versions_str = match.group(1) - versions = [] - for v in versions_str.split(','): - v = v.strip().strip("'\"") - if v: - versions.append(v) - - return versions + 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""" @@ -202,24 +194,17 @@ jobs: return final_versions, changes_made - def update_workflow_file(workflow_file, new_versions): - """Update the workflow file with new versions""" - content = Path(workflow_file).read_text() - - # Format new versions for YAML - versions_str = ", ".join([f"'{v}'" for v in new_versions]) - new_matrix_line = f" springboot-version: [ {versions_str} ]" - - # Replace the matrix line - pattern = r'(\s*)springboot-version:\s*\[\s*[^\]]+\s*\]' - replacement = new_matrix_line - - updated_content = re.sub(pattern, replacement, content) - - if updated_content != content: - Path(workflow_file).write_text(updated_content) + def update_json_file(json_file, new_versions): + """Update the JSON data file with new versions""" + try: + # Write new versions to JSON file + data = {"versions": new_versions} + with open(json_file, 'w') as f: + json.dump(data, f, indent=2) return True - return False + except Exception as e: + print(f"Error writing to {json_file}: {e}") + return False def main(): print("Fetching Spring Boot versions...") @@ -231,22 +216,22 @@ jobs: print(f"Found {len(all_versions)} versions") - workflows = [ - (".github/workflows/spring-boot-2-matrix.yml", "2"), - (".github/workflows/spring-boot-3-matrix.yml", "3"), - (".github/workflows/spring-boot-4-matrix.yml", "4") + 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 workflow_file, major_version in workflows: - if not Path(workflow_file).exists(): + for json_file, major_version in data_files: + if not Path(json_file).exists(): continue - print(f"\nProcessing {workflow_file} (Spring Boot {major_version}.x)") + print(f"\nProcessing {json_file} (Spring Boot {major_version}.x)") - current_versions = parse_current_versions(workflow_file) + current_versions = parse_current_versions(json_file) if not current_versions: continue @@ -256,7 +241,7 @@ jobs: if file_changed: print(f"New versions: {new_versions}") - if update_workflow_file(workflow_file, 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: From 82803d795b0332c2f9f05cd422a5d8559386092a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 15:56:56 +0200 Subject: [PATCH 11/15] skip unwanted files; fix description --- .../workflows/update-spring-boot-versions.yml | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 5ba3bc0c47..bcb7072a48 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -30,9 +30,11 @@ jobs: pip install requests packaging - name: Update Spring Boot versions + id: update_versions run: | cat << 'EOF' > update_versions.py import json + import os import re import requests from packaging import version @@ -248,13 +250,19 @@ jobs: print("No changes needed") if changes_made: - print(f"\nChanges made to workflows:") + print(f"\nChanges made to Spring Boot version files:") for change in change_summary: print(f" - {change}") - # Write summary for later use + # 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</dev/null || echo "See diff for changes") + ${{ steps.update_versions.outputs.changes_summary || 'See diff for changes' }} ### Update Strategy: - **Patch updates**: Updated to latest patch version of existing minor versions @@ -305,7 +317,7 @@ jobs: run: | if [ "${{ steps.changes.outputs.has_changes }}" = "true" ]; then echo "✅ Spring Boot version updates found and PR created" - cat version_changes.txt 2>/dev/null || true + echo "${{ steps.update_versions.outputs.changes_summary }}" else echo "ℹ️ No Spring Boot version updates needed" fi From 8def62e9d1df0925efd8b42ebd96d3eff5eab19a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 16:02:07 +0200 Subject: [PATCH 12/15] should now find latest version correctly --- .../workflows/update-spring-boot-versions.yml | 82 +++++++++++-------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index bcb7072a48..12fe780db7 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -43,61 +43,71 @@ jobs: def get_spring_boot_versions(): """Fetch all Spring Boot versions from Maven Central with retry logic""" - # Use the versions API instead of the general search - url = "https://search.maven.org/solrsearch/select" - params = { - "q": "g:org.springframework.boot AND a:spring-boot", - "core": "gav", - "rows": 500, - "wt": "json" - } - + max_retries = 3 - timeout = 60 # Increased timeout + timeout = 60 for attempt in range(max_retries): try: print(f"Fetching versions (attempt {attempt + 1}/{max_retries})...") - response = requests.get(url, params=params, timeout=timeout) + + # 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']): + 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}") + return sorted(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: {data}") + raise Exception(f"Unexpected API response structure") docs = data['response']['docs'] - print(f"Found {len(docs)} documents in response") + 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: - # Try different possible version field names - version_field = None - if 'v' in doc: - version_field = doc['v'] - elif 'version' in doc: - version_field = doc['version'] - elif 'latestVersion' in doc: - # This might be a summary doc, skip it - print(f"Skipping summary doc with latestVersion: {doc.get('latestVersion')}") - continue - else: - print(f"Warning: No version field found in doc: {doc}") - continue - - # Only include release versions (no SNAPSHOT, RC, M versions for 2.x and 3.x) - if not any(suffix in version_field for suffix in ['SNAPSHOT', 'RC', 'BUILD']): + 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']): versions.append(version_field) - print(f"Successfully fetched {len(versions)} versions") - return sorted(versions, key=version.parse) - except requests.exceptions.Timeout as e: - print(f"Attempt {attempt + 1} timed out: {e}") - if attempt < max_retries - 1: - print("Retrying...") - continue + if versions: + print(f"Successfully fetched {len(versions)} versions via search API") + return sorted(versions, key=version.parse) + except Exception as e: print(f"Attempt {attempt + 1} failed: {e}") if attempt < max_retries - 1: From fecd2b15349b3035d4e0d937183d195d2a92a02e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 16:06:47 +0200 Subject: [PATCH 13/15] fix version parsing --- .../workflows/update-spring-boot-versions.yml | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 12fe780db7..5c1adbfdeb 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -67,13 +67,24 @@ jobs: 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']): - versions.append(v) + 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}") - return sorted(versions, key=version.parse) + # 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...") @@ -101,12 +112,22 @@ jobs: 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']): + 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: - print(f"Successfully fetched {len(versions)} versions via search API") - return sorted(versions, key=version.parse) + # 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}") From d03b6c5d3cf69f111da3bfb1c05ad8efd3adc8f1 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 16:12:49 +0200 Subject: [PATCH 14/15] avoid duplicates --- .github/workflows/update-spring-boot-versions.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 5c1adbfdeb..684b482ce7 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -220,12 +220,23 @@ jobs: print(f"Removed second oldest version: {other_versions[0]}") changes_made = True - # Sort final versions + # 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 final_versions, changes_made + return deduplicated_versions, changes_made def update_json_file(json_file, new_versions): """Update the JSON data file with new versions""" From 2b77d4eb6c64b12a195efcaf92d6052144f3f455 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Sep 2025 16:15:44 +0200 Subject: [PATCH 15/15] format json --- .github/data/spring-boot-2-versions.json | 10 +++++++++- .github/data/spring-boot-3-versions.json | 8 +++++++- .github/data/spring-boot-4-versions.json | 6 +++++- .github/workflows/update-spring-boot-versions.yml | 5 +++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/data/spring-boot-2-versions.json b/.github/data/spring-boot-2-versions.json index 03917b60cd..15e163f1c2 100644 --- a/.github/data/spring-boot-2-versions.json +++ b/.github/data/spring-boot-2-versions.json @@ -1,3 +1,11 @@ { - "versions": ["2.1.0", "2.2.5", "2.4.13", "2.5.15", "2.6.15", "2.7.0", "2.7.18"] + "versions": [ + "2.1.0", + "2.2.5", + "2.4.13", + "2.5.15", + "2.6.15", + "2.7.0", + "2.7.18" + ] } diff --git a/.github/data/spring-boot-3-versions.json b/.github/data/spring-boot-3-versions.json index 558edf79f7..9c324fefa0 100644 --- a/.github/data/spring-boot-3-versions.json +++ b/.github/data/spring-boot-3-versions.json @@ -1,3 +1,9 @@ { - "versions": ["3.0.0", "3.2.10", "3.3.5", "3.4.5", "3.5.6"] + "versions": [ + "3.0.0", + "3.2.10", + "3.3.5", + "3.4.5", + "3.5.6" + ] } diff --git a/.github/data/spring-boot-4-versions.json b/.github/data/spring-boot-4-versions.json index 4a0c62e5c7..3716e4308e 100644 --- a/.github/data/spring-boot-4-versions.json +++ b/.github/data/spring-boot-4-versions.json @@ -1,3 +1,7 @@ { - "versions": ["4.0.0-M1", "4.0.0-M2", "4.0.0-M3"] + "versions": [ + "4.0.0-M1", + "4.0.0-M2", + "4.0.0-M3" + ] } diff --git a/.github/workflows/update-spring-boot-versions.yml b/.github/workflows/update-spring-boot-versions.yml index 684b482ce7..c1da8d930f 100644 --- a/.github/workflows/update-spring-boot-versions.yml +++ b/.github/workflows/update-spring-boot-versions.yml @@ -241,10 +241,11 @@ jobs: def update_json_file(json_file, new_versions): """Update the JSON data file with new versions""" try: - # Write new versions to JSON file + # 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) + 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}")