From 7994ffc568eb477305d1f88347ef5469690194d5 Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Sun, 3 May 2026 22:43:28 -0400 Subject: [PATCH 1/5] RewriteTest: framework-agnostic ExecutionContext customizer registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `RewriteTest.defaultExecutionContextCustomizers`, a `Map, Consumer>` applied to the context built by `defaultExecutionContext`. Test framework integrations (JUnit, TestNG, Spock, …) can register a customizer keyed by their own class without `rewrite-test` taking a dependency on any framework; `putIfAbsent` makes registration naturally idempotent. First consumer: a JUnit Jupiter `BeforeAllCallback` in rewrite-gradle's test sources, auto-detected via service loader, that loads `~/.m2/settings.xml` into the ExecutionContext. Recipes resolving Maven artifacts during rewrite-gradle tests then honor any configured mirror, sidestepping Maven Central rate limits (HTTP 404 + Retry-After) under parallel load. Gradle does not normally read the user's Maven settings, so this opt-in lives entirely in test sources of openrewrite/rewrite — not in any published API surface. --- .../MavenSettingsAutoLoadingExtension.java | 55 +++++++++++++++++++ .../org.junit.jupiter.api.extension.Extension | 1 + .../test/resources/junit-platform.properties | 1 + .../org/openrewrite/test/RewriteTest.java | 16 ++++++ 4 files changed, 73 insertions(+) create mode 100644 rewrite-gradle/src/test/java/org/openrewrite/gradle/MavenSettingsAutoLoadingExtension.java create mode 100644 rewrite-gradle/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 rewrite-gradle/src/test/resources/junit-platform.properties diff --git a/rewrite-gradle/src/test/java/org/openrewrite/gradle/MavenSettingsAutoLoadingExtension.java b/rewrite-gradle/src/test/java/org/openrewrite/gradle/MavenSettingsAutoLoadingExtension.java new file mode 100644 index 00000000000..6a4b588759e --- /dev/null +++ b/rewrite-gradle/src/test/java/org/openrewrite/gradle/MavenSettingsAutoLoadingExtension.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.gradle; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.openrewrite.ExecutionContext; +import org.openrewrite.maven.MavenExecutionContextView; +import org.openrewrite.maven.MavenSettings; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.maven.tree.MavenRepository.MAVEN_LOCAL_DEFAULT; + +/** + * Loads {@code ~/.m2/settings.xml} (and {@code $M2_HOME/conf/settings.xml}) into the + * default {@link ExecutionContext} for every {@link RewriteTest} in this module, so a + * configured Maven mirror or repository is honored by recipes that resolve artifacts. + * Auto-registered via {@code META-INF/services/org.junit.jupiter.api.extension.Extension}; + * Gradle does not normally read the user's Maven settings, so this is opt-in at the + * module test classpath level. + */ +public class MavenSettingsAutoLoadingExtension implements BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { + RewriteTest.defaultExecutionContextCustomizers.putIfAbsent( + MavenSettingsAutoLoadingExtension.class, + MavenSettingsAutoLoadingExtension::loadMavenSettings); + } + + private static void loadMavenSettings(ExecutionContext ctx) { + MavenExecutionContextView mctx = MavenExecutionContextView.view(ctx); + boolean nothingConfigured = mctx.getSettings() == null && + mctx.getLocalRepository().equals(MAVEN_LOCAL_DEFAULT) && + mctx.getRepositories().isEmpty() && + mctx.getActiveProfiles().isEmpty() && + mctx.getMirrors().isEmpty(); + if (nothingConfigured) { + mctx.setMavenSettings(MavenSettings.readMavenSettingsFromDisk(mctx)); + } + } +} diff --git a/rewrite-gradle/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/rewrite-gradle/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 00000000000..e2edebd662d --- /dev/null +++ b/rewrite-gradle/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +org.openrewrite.gradle.MavenSettingsAutoLoadingExtension diff --git a/rewrite-gradle/src/test/resources/junit-platform.properties b/rewrite-gradle/src/test/resources/junit-platform.properties new file mode 100644 index 00000000000..6efc0d5e85c --- /dev/null +++ b/rewrite-gradle/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.extensions.autodetection.enabled=true diff --git a/rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java b/rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java index c5d8264e5eb..e68ec8542b2 100644 --- a/rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java +++ b/rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java @@ -35,6 +35,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import java.util.function.Function; @@ -51,6 +52,18 @@ @SuppressWarnings("unused") public interface RewriteTest extends SourceSpecs { + /** + * Registry of customizers applied to the {@link ExecutionContext} produced by + * {@link #defaultExecutionContext(SourceSpec[])}. Test framework integrations + * (e.g. a JUnit Jupiter {@code BeforeAllCallback}) can register a customizer + * once at startup without {@code rewrite-test} taking a dependency on the + * framework. + *

+ * The map is keyed by the integration's class so {@code putIfAbsent(MyExt.class, MyExt::load)} + * is naturally idempotent — callers do not need their own one-shot guard. + */ + Map, Consumer> defaultExecutionContextCustomizers = new ConcurrentHashMap<>(); + static AdHocRecipe toRecipe(Supplier> visitor) { return new AdHocRecipe(null, null, null, visitor, null, null); } @@ -696,6 +709,9 @@ default ExecutionContext defaultExecutionContext(SourceSpec[] sourceSpecs) { } fail("Failed to parse sources or run recipe", t); }); + for (Consumer customizer : defaultExecutionContextCustomizers.values()) { + customizer.accept(ctx); + } return ParsingExecutionContextView.view(ctx).setCharset(StandardCharsets.UTF_8); } From a4ba99a7b70b442a3da054d2581d70efb8220704 Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Sun, 3 May 2026 22:47:53 -0400 Subject: [PATCH 2/5] ci: configure ~/.m2/settings.xml mirror to avoid Maven Central rate limits Inlines the build job that previously delegated to openrewrite/gh-automation's reusable ci-gradle workflow, and adds an `s4u/maven-settings-action` step that writes a `~/.m2/settings.xml` with a wildcard mirror pointing at Moderne's Artifactory cache. The MavenSettingsAutoLoadingExtension introduced earlier in this PR loads that file into the test ExecutionContext, so recipes resolving Maven artifacts during rewrite-gradle tests no longer hit Maven Central directly and avoid the HTTP 404 + Retry-After throttling that's been failing CI under parallel load. The reusable workflow had no hook to inject extra setup steps, and inlining keeps everything in this PR. Functionality (build, scheduled-failure Slack notification, snapshot publish on main) is preserved. --- .github/workflows/ci.yml | 77 ++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5f05691ed8..03bf5c57d2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,20 +18,67 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true +env: + GRADLE_SWITCHES: --console=plain --info --stacktrace --warning-mode=all --no-daemon + jobs: build: - uses: openrewrite/gh-automation/.github/workflows/ci-gradle.yml@main - with: - java_version: | - 25 - 21 - secrets: - gradle_enterprise_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - sonatype_username: ${{ secrets.SONATYPE_USERNAME }} - sonatype_token: ${{ secrets.SONATYPE_TOKEN}} - ossrh_signing_key: ${{ secrets.OSSRH_SIGNING_KEY }} - ossrh_signing_password: ${{ secrets.OSSRH_SIGNING_PASSWORD }} - OPS_GITHUB_ACTIONS_WEBHOOK: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }} - node_auth_token: ${{ secrets.NPM_TOKEN }} - pypi_token: ${{ secrets.PYPI_OPENREWRITE_PUBLISH }} - nuget_api_key: ${{ secrets.NUGET_API_KEY }} + runs-on: ubuntu-latest + if: github.event_name != 'schedule' || github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc' + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + show-progress: false + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: | + 25 + 21 + + - uses: astral-sh/setup-uv@v7 + + # Route Maven resolution through Moderne's Artifactory cache to avoid + # Maven Central rate-limiting (HTTP 404 + Retry-After) under parallel test + # load. Picked up by MavenSettingsAutoLoadingExtension at test time. Must + # run after setup-java because that action overwrites ~/.m2/settings.xml. + - uses: s4u/maven-settings-action@v3.1.0 + with: + mirrors: '[{"id": "moderne-cache", "name": "Moderne Artifactory Cache", "mirrorOf": "*", "url": "https://artifactory.moderne.ninja/artifactory/moderne-cache-3/"}]' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + + - name: build + run: ./gradlew ${{ env.GRADLE_SWITCHES }} build + + - name: slackNotificationOfFailure + if: failure() && github.event_name == 'schedule' && (github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc') + uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 + continue-on-error: true + env: + SLACK_WEBHOOK: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }} + MSG_MINIMAL: true + SLACK_MESSAGE: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SLACK_USERNAME: ${{ github.repository }} CI failure + SLACK_COLOR: failure + SLACK_FOOTER: '' + + - name: publish-snapshots + if: > + github.event_name != 'pull_request' && + github.ref == 'refs/heads/main' && + (github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc') + run: ./gradlew ${{ env.GRADLE_SWITCHES }} snapshot publish -PforceSigning -x test + env: + ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }} + ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_TOKEN }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_SIGNING_PASSWORD }} + ORG_GRADLE_PROJECT_nodeAuthToken: ${{ secrets.NPM_TOKEN }} + ORG_GRADLE_PROJECT_pypiToken: ${{ secrets.PYPI_OPENREWRITE_PUBLISH }} + ORG_GRADLE_PROJECT_nugetApiKey: ${{ secrets.NUGET_API_KEY }} From a34f26fc82adcc7884c617ed5b521da2433fe910 Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Sun, 3 May 2026 22:48:41 -0400 Subject: [PATCH 3/5] Revert "ci: configure ~/.m2/settings.xml mirror to avoid Maven Central rate limits" This reverts commit a4ba99a7b70b442a3da054d2581d70efb8220704. --- .github/workflows/ci.yml | 77 ++++++++-------------------------------- 1 file changed, 15 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03bf5c57d2e..f5f05691ed8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,67 +18,20 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true -env: - GRADLE_SWITCHES: --console=plain --info --stacktrace --warning-mode=all --no-daemon - jobs: build: - runs-on: ubuntu-latest - if: github.event_name != 'schedule' || github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc' - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - show-progress: false - - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: | - 25 - 21 - - - uses: astral-sh/setup-uv@v7 - - # Route Maven resolution through Moderne's Artifactory cache to avoid - # Maven Central rate-limiting (HTTP 404 + Retry-After) under parallel test - # load. Picked up by MavenSettingsAutoLoadingExtension at test time. Must - # run after setup-java because that action overwrites ~/.m2/settings.xml. - - uses: s4u/maven-settings-action@v3.1.0 - with: - mirrors: '[{"id": "moderne-cache", "name": "Moderne Artifactory Cache", "mirrorOf": "*", "url": "https://artifactory.moderne.ninja/artifactory/moderne-cache-3/"}]' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 - with: - develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - - - name: build - run: ./gradlew ${{ env.GRADLE_SWITCHES }} build - - - name: slackNotificationOfFailure - if: failure() && github.event_name == 'schedule' && (github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc') - uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 - continue-on-error: true - env: - SLACK_WEBHOOK: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }} - MSG_MINIMAL: true - SLACK_MESSAGE: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - SLACK_USERNAME: ${{ github.repository }} CI failure - SLACK_COLOR: failure - SLACK_FOOTER: '' - - - name: publish-snapshots - if: > - github.event_name != 'pull_request' && - github.ref == 'refs/heads/main' && - (github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc') - run: ./gradlew ${{ env.GRADLE_SWITCHES }} snapshot publish -PforceSigning -x test - env: - ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }} - ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_TOKEN }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_SIGNING_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_SIGNING_PASSWORD }} - ORG_GRADLE_PROJECT_nodeAuthToken: ${{ secrets.NPM_TOKEN }} - ORG_GRADLE_PROJECT_pypiToken: ${{ secrets.PYPI_OPENREWRITE_PUBLISH }} - ORG_GRADLE_PROJECT_nugetApiKey: ${{ secrets.NUGET_API_KEY }} + uses: openrewrite/gh-automation/.github/workflows/ci-gradle.yml@main + with: + java_version: | + 25 + 21 + secrets: + gradle_enterprise_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + sonatype_username: ${{ secrets.SONATYPE_USERNAME }} + sonatype_token: ${{ secrets.SONATYPE_TOKEN}} + ossrh_signing_key: ${{ secrets.OSSRH_SIGNING_KEY }} + ossrh_signing_password: ${{ secrets.OSSRH_SIGNING_PASSWORD }} + OPS_GITHUB_ACTIONS_WEBHOOK: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }} + node_auth_token: ${{ secrets.NPM_TOKEN }} + pypi_token: ${{ secrets.PYPI_OPENREWRITE_PUBLISH }} + nuget_api_key: ${{ secrets.NUGET_API_KEY }} From 6fdcea2ee3694fd704ae3ae537edb48d3c696f5f Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Mon, 4 May 2026 07:21:46 -0400 Subject: [PATCH 4/5] ci: configure Maven mirror via gh-automation composite actions Switches from the gh-automation reusable workflow (`ci-gradle.yml`) to its finer-grained composite-action form, so we can inject `s4u/maven-settings-action` between `setup` and `build`. The action writes a `~/.m2/settings.xml` whose wildcard mirror points at Moderne's Artifactory cache; the MavenSettingsAutoLoadingExtension introduced earlier in this PR loads it into the test ExecutionContext, avoiding Maven Central's HTTP 404 + Retry-After throttling under parallel test load. The composite actions land in https://github.com/openrewrite/gh-automation/pull/94. --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5f05691ed8..0da460b7982 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,18 +20,40 @@ concurrency: jobs: build: - uses: openrewrite/gh-automation/.github/workflows/ci-gradle.yml@main - with: - java_version: | - 25 - 21 - secrets: - gradle_enterprise_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - sonatype_username: ${{ secrets.SONATYPE_USERNAME }} - sonatype_token: ${{ secrets.SONATYPE_TOKEN}} - ossrh_signing_key: ${{ secrets.OSSRH_SIGNING_KEY }} - ossrh_signing_password: ${{ secrets.OSSRH_SIGNING_PASSWORD }} - OPS_GITHUB_ACTIONS_WEBHOOK: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }} - node_auth_token: ${{ secrets.NPM_TOKEN }} - pypi_token: ${{ secrets.PYPI_OPENREWRITE_PUBLISH }} - nuget_api_key: ${{ secrets.NUGET_API_KEY }} + runs-on: ubuntu-latest + if: github.event_name != 'schedule' || github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc' + steps: + - uses: openrewrite/gh-automation/.github/actions/setup@main + with: + java_version: | + 25 + 21 + develocity_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + + # Route Maven resolution through Moderne's Artifactory cache to avoid + # Maven Central rate-limiting (HTTP 404 + Retry-After) under parallel + # test load. Picked up by MavenSettingsAutoLoadingExtension at test time. + - uses: s4u/maven-settings-action@v3.1.0 + with: + mirrors: '[{"id": "moderne-cache", "name": "Moderne Artifactory Cache", "mirrorOf": "*", "url": "https://artifactory.moderne.ninja/artifactory/moderne-cache-3/"}]' + + - uses: openrewrite/gh-automation/.github/actions/build@main + + - if: failure() && github.event_name == 'schedule' && (github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc') + uses: openrewrite/gh-automation/.github/actions/slack-failure@main + with: + webhook: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }} + + - if: > + github.event_name != 'pull_request' && + github.ref == 'refs/heads/main' && + (github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc') + uses: openrewrite/gh-automation/.github/actions/publish-snapshots@main + with: + sonatype_username: ${{ secrets.SONATYPE_USERNAME }} + sonatype_token: ${{ secrets.SONATYPE_TOKEN }} + ossrh_signing_key: ${{ secrets.OSSRH_SIGNING_KEY }} + ossrh_signing_password: ${{ secrets.OSSRH_SIGNING_PASSWORD }} + node_auth_token: ${{ secrets.NPM_TOKEN }} + pypi_token: ${{ secrets.PYPI_OPENREWRITE_PUBLISH }} + nuget_api_key: ${{ secrets.NUGET_API_KEY }} From 6d0f47b36b0e4140f1eb1c6e22c2cdbafa940fbb Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Mon, 4 May 2026 07:40:23 -0400 Subject: [PATCH 5/5] ci: add Apache 2.0 header to junit-platform.properties for licenseTest --- .../src/test/resources/junit-platform.properties | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rewrite-gradle/src/test/resources/junit-platform.properties b/rewrite-gradle/src/test/resources/junit-platform.properties index 6efc0d5e85c..82e077a0bdb 100644 --- a/rewrite-gradle/src/test/resources/junit-platform.properties +++ b/rewrite-gradle/src/test/resources/junit-platform.properties @@ -1 +1,17 @@ +# +# Copyright 2026 the original author or authors. +#

+# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +#

+# https://www.apache.org/licenses/LICENSE-2.0 +#

+# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + junit.jupiter.extensions.autodetection.enabled=true