Skip to content

Commit 4864a79

Browse files
authored
RewriteTest: framework-agnostic ExecutionContext customizer registry (#7559)
* RewriteTest: framework-agnostic ExecutionContext customizer registry Adds `RewriteTest.defaultExecutionContextCustomizers`, a `Map<Class<?>, Consumer<ExecutionContext>>` 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. * 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. * Revert "ci: configure ~/.m2/settings.xml mirror to avoid Maven Central rate limits" This reverts commit a4ba99a. * 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 openrewrite/gh-automation#94. * ci: add Apache 2.0 header to junit-platform.properties for licenseTest
1 parent 6025024 commit 4864a79

5 files changed

Lines changed: 126 additions & 15 deletions

File tree

.github/workflows/ci.yml

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,40 @@ concurrency:
2020

2121
jobs:
2222
build:
23-
uses: openrewrite/gh-automation/.github/workflows/ci-gradle.yml@main
24-
with:
25-
java_version: |
26-
25
27-
21
28-
secrets:
29-
gradle_enterprise_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }}
30-
sonatype_username: ${{ secrets.SONATYPE_USERNAME }}
31-
sonatype_token: ${{ secrets.SONATYPE_TOKEN}}
32-
ossrh_signing_key: ${{ secrets.OSSRH_SIGNING_KEY }}
33-
ossrh_signing_password: ${{ secrets.OSSRH_SIGNING_PASSWORD }}
34-
OPS_GITHUB_ACTIONS_WEBHOOK: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }}
35-
node_auth_token: ${{ secrets.NPM_TOKEN }}
36-
pypi_token: ${{ secrets.PYPI_OPENREWRITE_PUBLISH }}
37-
nuget_api_key: ${{ secrets.NUGET_API_KEY }}
23+
runs-on: ubuntu-latest
24+
if: github.event_name != 'schedule' || github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc'
25+
steps:
26+
- uses: openrewrite/gh-automation/.github/actions/setup@main
27+
with:
28+
java_version: |
29+
25
30+
21
31+
develocity_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }}
32+
33+
# Route Maven resolution through Moderne's Artifactory cache to avoid
34+
# Maven Central rate-limiting (HTTP 404 + Retry-After) under parallel
35+
# test load. Picked up by MavenSettingsAutoLoadingExtension at test time.
36+
- uses: s4u/maven-settings-action@v3.1.0
37+
with:
38+
mirrors: '[{"id": "moderne-cache", "name": "Moderne Artifactory Cache", "mirrorOf": "*", "url": "https://artifactory.moderne.ninja/artifactory/moderne-cache-3/"}]'
39+
40+
- uses: openrewrite/gh-automation/.github/actions/build@main
41+
42+
- if: failure() && github.event_name == 'schedule' && (github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc')
43+
uses: openrewrite/gh-automation/.github/actions/slack-failure@main
44+
with:
45+
webhook: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }}
46+
47+
- if: >
48+
github.event_name != 'pull_request' &&
49+
github.ref == 'refs/heads/main' &&
50+
(github.repository_owner == 'openrewrite' || github.repository_owner == 'moderneinc')
51+
uses: openrewrite/gh-automation/.github/actions/publish-snapshots@main
52+
with:
53+
sonatype_username: ${{ secrets.SONATYPE_USERNAME }}
54+
sonatype_token: ${{ secrets.SONATYPE_TOKEN }}
55+
ossrh_signing_key: ${{ secrets.OSSRH_SIGNING_KEY }}
56+
ossrh_signing_password: ${{ secrets.OSSRH_SIGNING_PASSWORD }}
57+
node_auth_token: ${{ secrets.NPM_TOKEN }}
58+
pypi_token: ${{ secrets.PYPI_OPENREWRITE_PUBLISH }}
59+
nuget_api_key: ${{ secrets.NUGET_API_KEY }}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.gradle;
17+
18+
import org.junit.jupiter.api.extension.BeforeAllCallback;
19+
import org.junit.jupiter.api.extension.ExtensionContext;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.maven.MavenExecutionContextView;
22+
import org.openrewrite.maven.MavenSettings;
23+
import org.openrewrite.test.RewriteTest;
24+
25+
import static org.openrewrite.maven.tree.MavenRepository.MAVEN_LOCAL_DEFAULT;
26+
27+
/**
28+
* Loads {@code ~/.m2/settings.xml} (and {@code $M2_HOME/conf/settings.xml}) into the
29+
* default {@link ExecutionContext} for every {@link RewriteTest} in this module, so a
30+
* configured Maven mirror or repository is honored by recipes that resolve artifacts.
31+
* Auto-registered via {@code META-INF/services/org.junit.jupiter.api.extension.Extension};
32+
* Gradle does not normally read the user's Maven settings, so this is opt-in at the
33+
* module test classpath level.
34+
*/
35+
public class MavenSettingsAutoLoadingExtension implements BeforeAllCallback {
36+
37+
@Override
38+
public void beforeAll(ExtensionContext context) {
39+
RewriteTest.defaultExecutionContextCustomizers.putIfAbsent(
40+
MavenSettingsAutoLoadingExtension.class,
41+
MavenSettingsAutoLoadingExtension::loadMavenSettings);
42+
}
43+
44+
private static void loadMavenSettings(ExecutionContext ctx) {
45+
MavenExecutionContextView mctx = MavenExecutionContextView.view(ctx);
46+
boolean nothingConfigured = mctx.getSettings() == null &&
47+
mctx.getLocalRepository().equals(MAVEN_LOCAL_DEFAULT) &&
48+
mctx.getRepositories().isEmpty() &&
49+
mctx.getActiveProfiles().isEmpty() &&
50+
mctx.getMirrors().isEmpty();
51+
if (nothingConfigured) {
52+
mctx.setMavenSettings(MavenSettings.readMavenSettingsFromDisk(mctx));
53+
}
54+
}
55+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.openrewrite.gradle.MavenSettingsAutoLoadingExtension
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#
2+
# Copyright 2026 the original author or authors.
3+
# <p>
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
# <p>
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
# <p>
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
junit.jupiter.extensions.autodetection.enabled=true

rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.nio.charset.StandardCharsets;
3636
import java.nio.file.Path;
3737
import java.util.*;
38+
import java.util.concurrent.ConcurrentHashMap;
3839
import java.util.concurrent.CopyOnWriteArrayList;
3940
import java.util.function.Consumer;
4041
import java.util.function.Function;
@@ -51,6 +52,18 @@
5152

5253
@SuppressWarnings("unused")
5354
public interface RewriteTest extends SourceSpecs {
55+
/**
56+
* Registry of customizers applied to the {@link ExecutionContext} produced by
57+
* {@link #defaultExecutionContext(SourceSpec[])}. Test framework integrations
58+
* (e.g. a JUnit Jupiter {@code BeforeAllCallback}) can register a customizer
59+
* once at startup without {@code rewrite-test} taking a dependency on the
60+
* framework.
61+
* <p>
62+
* The map is keyed by the integration's class so {@code putIfAbsent(MyExt.class, MyExt::load)}
63+
* is naturally idempotent — callers do not need their own one-shot guard.
64+
*/
65+
Map<Class<?>, Consumer<ExecutionContext>> defaultExecutionContextCustomizers = new ConcurrentHashMap<>();
66+
5467
static AdHocRecipe toRecipe(Supplier<TreeVisitor<?, ExecutionContext>> visitor) {
5568
return new AdHocRecipe(null, null, null, visitor, null, null);
5669
}
@@ -696,6 +709,9 @@ default ExecutionContext defaultExecutionContext(SourceSpec<?>[] sourceSpecs) {
696709
}
697710
fail("Failed to parse sources or run recipe", t);
698711
});
712+
for (Consumer<ExecutionContext> customizer : defaultExecutionContextCustomizers.values()) {
713+
customizer.accept(ctx);
714+
}
699715
return ParsingExecutionContextView.view(ctx).setCharset(StandardCharsets.UTF_8);
700716
}
701717

0 commit comments

Comments
 (0)