Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ private void collectBasePaths(MavenProject project, Set<Path> paths, Path localR
if (getRecipeArtifactCoordinates().isEmpty()) {
return null;
}
ArtifactResolver resolver = new ArtifactResolver(repositorySystem, mavenSession);
ArtifactResolver resolver = new ArtifactResolver(repositorySystem, mavenSession, getLog());

Set<Artifact> artifacts = new HashSet<>();
for (String coordinate : getRecipeArtifactCoordinates()) {
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/org/openrewrite/maven/ArtifactResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.apache.maven.RepositoryUtils;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
Expand All @@ -43,11 +44,13 @@ public class ArtifactResolver {
private final RepositorySystem repositorySystem;
private final RepositorySystemSession repositorySystemSession;
private final List<RemoteRepository> remoteRepositories;
private final Log log;

public ArtifactResolver(RepositorySystem repositorySystem, MavenSession session) {
public ArtifactResolver(RepositorySystem repositorySystem, MavenSession session, Log log) {
this.repositorySystem = repositorySystem;
this.repositorySystemSession = session.getRepositorySession();
this.remoteRepositories = RepositoryUtils.toRepos(session.getCurrentProject().getRemoteArtifactRepositories());
this.log = log;
}

public Artifact createArtifact(String coordinates) throws MojoExecutionException {
Expand Down Expand Up @@ -77,6 +80,13 @@ public Set<Artifact> resolveArtifactsAndDependencies(Set<Artifact> artifacts) th
for (ArtifactResult resolved : dependencyResult.getArtifactResults()) {
elements.add(resolved.getArtifact());
}

String warning = CodeGenomeProjectWarning.warningFor(artifacts, dependencyResult.getArtifactResults());
if (warning != null) {
for (String line : warning.split("\n")) {
log.warn(line);
}
}
return elements;
} catch (DependencyResolutionException e) {
throw new MojoExecutionException("Failed to resolve requested artifacts transitive dependencies.", e);
Expand Down
118 changes: 118 additions & 0 deletions src/main/java/org/openrewrite/maven/CodeGenomeProjectWarning.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.maven;

import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.repository.ArtifactRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactResult;
import org.jspecify.annotations.Nullable;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

/**
* New OpenRewrite and Moderne recipe releases are published to the Code Genome Project rather than to Maven Central.
* Releases already on Maven Central remain there, so a build that resolves recipes with a dynamic version against
* Maven Central keeps succeeding while silently pinning itself to the last release published there.
* <p>
* This detects that situation so the plugin can point the user at the Code Genome Project. It is informational only.
*/
final class CodeGenomeProjectWarning {

static final String CREDENTIALS_DOCS = "https://codegenomeproject.org/token";

private CodeGenomeProjectWarning() {
}

/**
* @param requestedRecipeArtifacts the {@code rewrite.recipeArtifactCoordinates} the build asked for
* @param results the outcome of resolving those coordinates and their dependencies
* @return a warning to log, or {@code null} when recipes cannot be silently stale
*/
static @Nullable String warningFor(Collection<Artifact> requestedRecipeArtifacts, Collection<ArtifactResult> results) {
Map<String, String> dynamicVersions = new HashMap<>();
for (Artifact artifact : requestedRecipeArtifacts) {
if (isRecipeArtifact(artifact.getGroupId()) && isDynamicVersion(artifact.getVersion())) {
dynamicVersions.put(artifact.getGroupId() + ":" + artifact.getArtifactId(), artifact.getVersion());
}
}
if (dynamicVersions.isEmpty()) {
return null;
}

Set<String> stale = new LinkedHashSet<>();
for (ArtifactResult result : results) {
Artifact resolved = result.getArtifact();
if (resolved == null || !isMavenCentral(result.getRepository())) {
continue;
}
String requestedVersion = dynamicVersions.get(resolved.getGroupId() + ":" + resolved.getArtifactId());
if (requestedVersion != null) {
stale.add(resolved.getGroupId() + ":" + resolved.getArtifactId() + ":" + requestedVersion +
" resolved to " + resolved.getVersion());
}
}
if (stale.isEmpty()) {
return null;
}

StringBuilder warning = new StringBuilder("These recipe artifacts resolve from Maven Central, which no longer receives new recipe releases:");
for (String artifact : stale) {
warning.append("\n ").append(artifact);
}
return warning
.append("\nNewer recipe versions are published to the Code Genome Project; configure it in your repositories to stop resolving stale recipes.")
.append("\nSee ").append(CREDENTIALS_DOCS).append(" for credentials and repository configuration.")
.toString();
}

private static boolean isRecipeArtifact(String group) {
return "org.openrewrite".equals(group) || group.startsWith("org.openrewrite.") ||
"io.moderne".equals(group) || group.startsWith("io.moderne.");
}

/**
* A version the user deliberately pinned is left alone; only a version that asks for "whatever is newest" can
* quietly resolve to the final Maven Central release.
*/
private static boolean isDynamicVersion(String version) {
return "LATEST".equals(version) || "RELEASE".equals(version) ||
version.startsWith("[") || version.startsWith("(");
}

/**
* Unlike Gradle, Maven reports which repository an artifact was actually served by, even when it came from the
* local repository cache. An internal mirror is substituted into the project's repositories before resolution, so
* it is reported under its own URL rather than as Maven Central.
*/
private static boolean isMavenCentral(@Nullable ArtifactRepository repository) {
if (!(repository instanceof RemoteRepository)) {
return false;
}
try {
String host = new URI(((RemoteRepository) repository).getUrl()).getHost();
if (host == null) {
return false;
}
host = host.toLowerCase(Locale.ROOT);
return "repo.maven.apache.org".equals(host) || "repo1.maven.org".equals(host) || "repo2.maven.org".equals(host);
} catch (URISyntaxException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void execute() throws MojoExecutionException {
}

private Set<Artifact> resolveArtifacts(String recipeArtifactCoordinate) throws MojoExecutionException {
ArtifactResolver resolver = new ArtifactResolver(repositorySystem, mavenSession);
ArtifactResolver resolver = new ArtifactResolver(repositorySystem, mavenSession, getLog());
Artifact artifact = resolver.createArtifact(recipeArtifactCoordinate);
return resolver.resolveArtifactsAndDependencies(singleton(artifact));
}
Expand Down
131 changes: 131 additions & 0 deletions src/test/java/org/openrewrite/maven/CodeGenomeProjectWarningTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.maven;

import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.repository.ArtifactRepository;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResult;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;

class CodeGenomeProjectWarningTest {

private static final RemoteRepository MAVEN_CENTRAL = repository("https://repo.maven.apache.org/maven2/");

@ParameterizedTest
@ValueSource(strings = {"LATEST", "RELEASE", "[6.0,7.0)", "(,7.0)"})
void warnOnDynamicVersionsResolvedFromMavenCentral(String version) {
String warning = CodeGenomeProjectWarning.warningFor(
singletonList(artifact("org.openrewrite.recipe", "rewrite-spring", version)),
singletonList(resolvedFrom(artifact("org.openrewrite.recipe", "rewrite-spring", "6.15.0"), MAVEN_CENTRAL)));

assertThat(warning)
.contains("org.openrewrite.recipe:rewrite-spring:" + version)
.contains("resolved to 6.15.0")
.contains(CodeGenomeProjectWarning.CREDENTIALS_DOCS);
}

@Test
void warnOnModerneRecipeArtifacts() {
assertThat(CodeGenomeProjectWarning.warningFor(
singletonList(artifact("io.moderne.recipe", "rewrite-spring", "LATEST")),
singletonList(resolvedFrom(artifact("io.moderne.recipe", "rewrite-spring", "6.15.0"), MAVEN_CENTRAL))))
.contains("io.moderne.recipe:rewrite-spring:LATEST");
}

@Test
void noWarningOnPinnedVersions() {
Artifact pinned = artifact("org.openrewrite.recipe", "rewrite-spring", "6.15.0");

assertThat(CodeGenomeProjectWarning.warningFor(
singletonList(pinned), singletonList(resolvedFrom(pinned, MAVEN_CENTRAL))))
.isNull();
}

@Test
void noWarningOnUnrelatedArtifacts() {
assertThat(CodeGenomeProjectWarning.warningFor(
singletonList(artifact("com.example", "my-recipes", "LATEST")),
singletonList(resolvedFrom(artifact("com.example", "my-recipes", "1.0.0"), MAVEN_CENTRAL))))
.isNull();
}

@Test
void noWarningWithoutRecipeArtifacts() {
assertThat(CodeGenomeProjectWarning.warningFor(emptyList(), emptyList())).isNull();
}

@Test
void noWarningBehindAnInternalMirror() {
assertThat(CodeGenomeProjectWarning.warningFor(
singletonList(artifact("org.openrewrite.recipe", "rewrite-spring", "LATEST")),
singletonList(resolvedFrom(artifact("org.openrewrite.recipe", "rewrite-spring", "6.15.0"),
repository("https://artifacts.internal.example.com/maven-central")))))
.isNull();
}

@Test
void noWarningWhenResolvedFromTheCodeGenomeProject() {
assertThat(CodeGenomeProjectWarning.warningFor(
singletonList(artifact("org.openrewrite.recipe", "rewrite-spring", "LATEST")),
singletonList(resolvedFrom(artifact("org.openrewrite.recipe", "rewrite-spring", "6.15.0"),
repository("https://artifacts.codegenomeproject.org/maven")))))
.isNull();
}

@Test
void noWarningForTransitiveDependenciesOfAPinnedRecipeArtifact() {
assertThat(CodeGenomeProjectWarning.warningFor(
singletonList(artifact("org.openrewrite.recipe", "rewrite-spring", "6.15.0")),
singletonList(resolvedFrom(artifact("org.openrewrite", "rewrite-java", "8.84.0"), MAVEN_CENTRAL))))
.isNull();
}

@Test
void noWarningWhenTheResolvingRepositoryIsNotRemote() {
assertThat(CodeGenomeProjectWarning.warningFor(
singletonList(artifact("org.openrewrite.recipe", "rewrite-spring", "LATEST")),
singletonList(resolvedFrom(artifact("org.openrewrite.recipe", "rewrite-spring", "6.15.0"),
new LocalRepository("target/local-repo")))))
.isNull();
}

private static Artifact artifact(String groupId, String artifactId, String version) {
return new DefaultArtifact(groupId, artifactId, null, "jar", version);
}

private static RemoteRepository repository(String url) {
return new RemoteRepository.Builder("test", "default", url).build();
}

private static ArtifactResult resolvedFrom(Artifact artifact, ArtifactRepository repository) {
ArtifactRequest request = new ArtifactRequest();
request.setArtifact(artifact);
ArtifactResult result = new ArtifactResult(request);
result.setArtifact(artifact);
result.setRepository(repository);
return result;
}
}
49 changes: 49 additions & 0 deletions src/test/java/org/openrewrite/maven/StaleRecipeArtifactIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.maven;

import com.soebes.itf.jupiter.extension.*;
import com.soebes.itf.jupiter.maven.MavenExecutionResult;

import static com.soebes.itf.extension.assertj.MavenITAssertions.assertThat;

@MavenGoal("${project.groupId}:${project.artifactId}:${project.version}:discover")
@MavenJupiterExtension
@MavenOption(MavenCLIOptions.NO_TRANSFER_PROGRESS)
@MavenOption(MavenCLIExtra.MUTE_PLUGIN_VALIDATION_WARNING)
class StaleRecipeArtifactIT {

@MavenTest
@SystemProperty(value = "rewrite.recipeArtifactCoordinates", content = "org.openrewrite.recipe:rewrite-testing-frameworks:LATEST")
void dynamic_recipe_version(MavenExecutionResult result) {
assertThat(result)
.isSuccessful()
.out()
.warn()
.anySatisfy(line -> assertThat(line).contains("org.openrewrite.recipe:rewrite-testing-frameworks:LATEST"))
.anySatisfy(line -> assertThat(line).contains(CodeGenomeProjectWarning.CREDENTIALS_DOCS));
}

@MavenTest
@SystemProperty(value = "rewrite.recipeArtifactCoordinates", content = "org.openrewrite.recipe:rewrite-testing-frameworks:3.42.1")
void pinned_recipe_version(MavenExecutionResult result) {
assertThat(result)
.isSuccessful()
.out()
.warn()
.allSatisfy(line -> assertThat(line).doesNotContain(CodeGenomeProjectWarning.CREDENTIALS_DOCS));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.openrewrite.maven</groupId>
<artifactId>dynamic_recipe_version</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>StaleRecipeArtifactIT#dynamic_recipe_version</name>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.openrewrite.maven</groupId>
<artifactId>pinned_recipe_version</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>StaleRecipeArtifactIT#pinned_recipe_version</name>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
Loading