Skip to content

Commit 970b2b4

Browse files
authored
Merge pull request #1500 from PratikMane0112/feature/add-incremental-recipe
Add incrementals recipe
2 parents 2c492ea + 0e34938 commit 970b2b4

File tree

9 files changed

+619
-1
lines changed

9 files changed

+619
-1
lines changed

plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/config/Settings.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,14 @@ public static String getWiremockVersion() {
246246
return readProperty("wiremock.version", "versions.properties");
247247
}
248248

249+
/**
250+
* Return the Incrementals git-changelist-maven-extension version
251+
* @return The Incrementals extension version
252+
*/
253+
public static String getIncrementalExtensionVersion() {
254+
return readProperty("git-changelist-maven-extension.version", "versions.properties");
255+
}
256+
249257
public static String getJenkinsMinimumBaseline() {
250258
String jenkinsVersion = getJenkinsMinimumVersion();
251259
if (JENKINS_VERSION_LTS_PATTERN.test(jenkinsVersion)) {
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package io.jenkins.tools.pluginmodernizer.core.recipes;
2+
3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4+
import io.jenkins.tools.pluginmodernizer.core.config.Settings;
5+
import io.jenkins.tools.pluginmodernizer.core.extractor.ArchetypeCommonFile;
6+
import io.jenkins.tools.pluginmodernizer.core.visitors.AddIncrementalsVisitor;
7+
import java.util.ArrayList;
8+
import java.util.Collection;
9+
import java.util.Collections;
10+
import java.util.stream.Collectors;
11+
import org.intellij.lang.annotations.Language;
12+
import org.openrewrite.ExecutionContext;
13+
import org.openrewrite.ScanningRecipe;
14+
import org.openrewrite.SourceFile;
15+
import org.openrewrite.Tree;
16+
import org.openrewrite.TreeVisitor;
17+
import org.openrewrite.text.PlainTextParser;
18+
import org.openrewrite.xml.XmlParser;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
/**
23+
* Recipe to enable incrementals in a Jenkins plugin.
24+
* Transforms the POM to use Git-based versioning and creates required .mvn files.
25+
*/
26+
@SuppressFBWarnings(value = "VA_FORMAT_STRING_USES_NEWLINE", justification = "Newline is used for formatting")
27+
public class AddIncrementals extends ScanningRecipe<AddIncrementals.ConfigState> {
28+
29+
private static final Logger LOG = LoggerFactory.getLogger(AddIncrementals.class);
30+
31+
@Language("xml")
32+
private static final String MAVEN_EXTENSIONS_TEMPLATE = """
33+
<?xml version="1.0" encoding="UTF-8"?>
34+
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
35+
<extension>
36+
<groupId>io.jenkins.tools.incrementals</groupId>
37+
<artifactId>git-changelist-maven-extension</artifactId>
38+
<version>%s</version>
39+
</extension>
40+
</extensions>
41+
""";
42+
43+
@Language("txt")
44+
private static final String MAVEN_CONFIG_TEMPLATE = "-Pconsume-incrementals\n-Pmight-produce-incrementals\n";
45+
46+
@Override
47+
public String getDisplayName() {
48+
return "Add incrementals";
49+
}
50+
51+
@Override
52+
public String getDescription() {
53+
return "Enables incrementals by transforming POM version structure and creating .mvn configuration files.";
54+
}
55+
56+
@Override
57+
public ConfigState getInitialValue(ExecutionContext ctx) {
58+
return new ConfigState();
59+
}
60+
61+
@Override
62+
public TreeVisitor<?, ExecutionContext> getScanner(ConfigState state) {
63+
return new TreeVisitor<>() {
64+
@Override
65+
public Tree visit(Tree tree, ExecutionContext ctx) {
66+
if (tree instanceof SourceFile sourceFile) {
67+
if (ArchetypeCommonFile.MAVEN_CONFIG.same(sourceFile.getSourcePath())) {
68+
LOG.debug(".mvn/maven.config already exists. Marking as present.");
69+
state.setMavenConfigExists(true);
70+
}
71+
if (ArchetypeCommonFile.MAVEN_EXTENSIONS.same(sourceFile.getSourcePath())) {
72+
LOG.debug(".mvn/extensions.xml already exists. Marking as present.");
73+
state.setMavenExtensionsExists(true);
74+
}
75+
if (ArchetypeCommonFile.POM.same(sourceFile.getSourcePath())) {
76+
LOG.debug("POM file found. Will be processed by visitor.");
77+
state.setPomExists(true);
78+
}
79+
}
80+
return tree;
81+
}
82+
};
83+
}
84+
85+
@Override
86+
public TreeVisitor<?, ExecutionContext> getVisitor(ConfigState state) {
87+
return new AddIncrementalsVisitor();
88+
}
89+
90+
@Override
91+
public Collection<SourceFile> generate(ConfigState state, ExecutionContext ctx) {
92+
if (!state.isPomExists()) {
93+
LOG.warn("No pom.xml found. Cannot generate .mvn files.");
94+
return Collections.emptyList();
95+
}
96+
97+
Collection<SourceFile> generatedFiles = new ArrayList<>();
98+
99+
if (!state.isMavenConfigExists()) {
100+
LOG.debug("Generating .mvn/maven.config");
101+
generatedFiles.addAll(PlainTextParser.builder()
102+
.build()
103+
.parse(MAVEN_CONFIG_TEMPLATE)
104+
.map(brandNewFile ->
105+
(SourceFile) brandNewFile.withSourcePath(ArchetypeCommonFile.MAVEN_CONFIG.getPath()))
106+
.collect(Collectors.toList()));
107+
}
108+
109+
if (!state.isMavenExtensionsExists()) {
110+
LOG.debug("Generating .mvn/extensions.xml");
111+
String extensionsXml = String.format(MAVEN_EXTENSIONS_TEMPLATE, Settings.getIncrementalExtensionVersion());
112+
generatedFiles.addAll(XmlParser.builder()
113+
.build()
114+
.parse(extensionsXml)
115+
.map(brandNewFile ->
116+
(SourceFile) brandNewFile.withSourcePath(ArchetypeCommonFile.MAVEN_EXTENSIONS.getPath()))
117+
.collect(Collectors.toList()));
118+
}
119+
120+
return generatedFiles;
121+
}
122+
123+
/**
124+
* Configuration state for the recipe
125+
*/
126+
public static class ConfigState {
127+
private boolean mavenConfigExists = false;
128+
private boolean mavenExtensionsExists = false;
129+
private boolean pomExists = false;
130+
131+
public boolean isMavenConfigExists() {
132+
return mavenConfigExists;
133+
}
134+
135+
public void setMavenConfigExists(boolean value) {
136+
mavenConfigExists = value;
137+
}
138+
139+
public boolean isMavenExtensionsExists() {
140+
return mavenExtensionsExists;
141+
}
142+
143+
public void setMavenExtensionsExists(boolean value) {
144+
mavenExtensionsExists = value;
145+
}
146+
147+
public boolean isPomExists() {
148+
return pomExists;
149+
}
150+
151+
public void setPomExists(boolean value) {
152+
pomExists = value;
153+
}
154+
}
155+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package io.jenkins.tools.pluginmodernizer.core.visitors;
2+
3+
import io.jenkins.tools.pluginmodernizer.core.recipes.AddProperty;
4+
import java.util.Optional;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
7+
import org.openrewrite.ExecutionContext;
8+
import org.openrewrite.maven.MavenIsoVisitor;
9+
import org.openrewrite.xml.ChangeTagValueVisitor;
10+
import org.openrewrite.xml.tree.Xml;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
/**
15+
* Visitor to modify POM for incrementals support.
16+
* Transforms version structure and adds required properties.
17+
*/
18+
public class AddIncrementalsVisitor extends MavenIsoVisitor<ExecutionContext> {
19+
20+
private static final Logger LOG = LoggerFactory.getLogger(AddIncrementalsVisitor.class);
21+
private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+(?:\\.\\d+)*)(.*|$)");
22+
private static final Pattern GITHUB_PATTERN =
23+
Pattern.compile("github\\.com[:/]([^/]+/[^/]+?)(?:\\.git)?$", Pattern.CASE_INSENSITIVE);
24+
25+
@Override
26+
public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) {
27+
document = super.visitDocument(document, ctx);
28+
29+
Xml.Tag root = document.getRoot();
30+
31+
// Check if properties section exists
32+
Optional<Xml.Tag> propertiesTag = root.getChild("properties");
33+
if (propertiesTag.isEmpty()) {
34+
LOG.warn("POM lacks a properties section. Cannot add incrementals properties. Skipping transformation.");
35+
return document;
36+
}
37+
38+
// Check if already using incrementals format
39+
Optional<Xml.Tag> versionTag = root.getChild("version");
40+
if (versionTag.isPresent()) {
41+
String currentVersion = versionTag.get().getValue().orElse("");
42+
if (currentVersion.contains("${revision}") || currentVersion.contains("${changelist}")) {
43+
LOG.info("POM already uses incrementals version format. Skipping transformation.");
44+
return document;
45+
}
46+
47+
// Extract revision from current version
48+
Matcher versionMatcher = VERSION_PATTERN.matcher(currentVersion);
49+
String revision = "1";
50+
if (versionMatcher.find()) {
51+
revision = versionMatcher.group(1);
52+
}
53+
54+
// Update version to ${revision}${changelist} format
55+
LOG.debug("Transforming version from {} to ${{revision}}${{changelist}}", currentVersion);
56+
document = (Xml.Document) new ChangeTagValueVisitor<>(versionTag.get(), "${revision}${changelist}")
57+
.visitNonNull(document, ctx);
58+
59+
// Add properties if they don't exist
60+
document = (Xml.Document)
61+
new AddProperty("revision", revision).getVisitor().visitNonNull(document, ctx);
62+
document = (Xml.Document)
63+
new AddProperty("changelist", "-SNAPSHOT").getVisitor().visitNonNull(document, ctx);
64+
65+
// Extract and add GitHub repo from SCM
66+
Optional<Xml.Tag> scmTag = root.getChild("scm");
67+
String gitHubRepo = null;
68+
if (scmTag.isPresent()) {
69+
gitHubRepo = extractGitHubRepo(scmTag.get());
70+
if (gitHubRepo != null) {
71+
LOG.debug("Adding gitHubRepo property: {}", gitHubRepo);
72+
document = (Xml.Document) new AddProperty("gitHubRepo", gitHubRepo)
73+
.getVisitor()
74+
.visitNonNull(document, ctx);
75+
76+
// Update SCM URLs to use ${gitHubRepo} property
77+
document = updateScmUrls(document, gitHubRepo, ctx);
78+
}
79+
80+
// Add scmTag property
81+
document = (Xml.Document)
82+
new AddProperty("scmTag", "HEAD").getVisitor().visitNonNull(document, ctx);
83+
84+
// Refresh root tag reference to avoid stale references after document transformations
85+
root = document.getRoot();
86+
scmTag = root.getChild("scm");
87+
88+
// Update SCM tag to use ${scmTag}
89+
Optional<Xml.Tag> tagTag = scmTag.isPresent() ? scmTag.get().getChild("tag") : Optional.empty();
90+
if (tagTag.isPresent()) {
91+
document = (Xml.Document)
92+
new ChangeTagValueVisitor<>(tagTag.get(), "${scmTag}").visitNonNull(document, ctx);
93+
}
94+
}
95+
96+
// Only rewrite the URL if the gitHubRepo property was successfully added
97+
if (gitHubRepo != null) {
98+
Optional<Xml.Tag> urlTag = root.getChild("url");
99+
if (urlTag.isPresent()) {
100+
String url = urlTag.get().getValue().orElse("");
101+
Matcher urlMatcher = GITHUB_PATTERN.matcher(url);
102+
if (urlMatcher.find()) {
103+
document = (Xml.Document)
104+
new ChangeTagValueVisitor<>(urlTag.get(), "https://github.com/${gitHubRepo}")
105+
.visitNonNull(document, ctx);
106+
}
107+
}
108+
}
109+
}
110+
111+
return document;
112+
}
113+
114+
/**
115+
* Extract GitHub repository from SCM tag
116+
*/
117+
private String extractGitHubRepo(Xml.Tag scmTag) {
118+
// Try connection tag first
119+
Optional<Xml.Tag> connectionTag = scmTag.getChild("connection");
120+
if (connectionTag.isPresent()) {
121+
String connection = connectionTag.get().getValue().orElse("");
122+
Matcher matcher = GITHUB_PATTERN.matcher(connection);
123+
if (matcher.find()) {
124+
return matcher.group(1);
125+
}
126+
}
127+
128+
// Fall back to developerConnection tag
129+
Optional<Xml.Tag> developerConnectionTag = scmTag.getChild("developerConnection");
130+
if (developerConnectionTag.isPresent()) {
131+
String developerConnection = developerConnectionTag.get().getValue().orElse("");
132+
Matcher matcher = GITHUB_PATTERN.matcher(developerConnection);
133+
if (matcher.find()) {
134+
return matcher.group(1);
135+
}
136+
}
137+
138+
// Fall back to url tag
139+
Optional<Xml.Tag> urlTag = scmTag.getChild("url");
140+
if (urlTag.isPresent()) {
141+
String url = urlTag.get().getValue().orElse("");
142+
Matcher matcher = GITHUB_PATTERN.matcher(url);
143+
if (matcher.find()) {
144+
return matcher.group(1);
145+
}
146+
}
147+
148+
return null;
149+
}
150+
151+
/**
152+
* Update SCM URLs to use ${gitHubRepo} property
153+
*/
154+
private Xml.Document updateScmUrls(Xml.Document document, String gitHubRepo, ExecutionContext ctx) {
155+
Xml.Tag root = document.getRoot();
156+
Optional<Xml.Tag> scmTag = root.getChild("scm");
157+
if (scmTag.isEmpty()) {
158+
return document;
159+
}
160+
161+
// Update connection
162+
Optional<Xml.Tag> connectionTag = scmTag.get().getChild("connection");
163+
if (connectionTag.isPresent()) {
164+
document = (Xml.Document)
165+
new ChangeTagValueVisitor<>(connectionTag.get(), "scm:git:https://github.com/${gitHubRepo}.git")
166+
.visitNonNull(document, ctx);
167+
// Refresh references after document transformation
168+
root = document.getRoot();
169+
scmTag = root.getChild("scm");
170+
}
171+
172+
// Update developerConnection
173+
Optional<Xml.Tag> devConnectionTag =
174+
scmTag.isPresent() ? scmTag.get().getChild("developerConnection") : Optional.empty();
175+
if (devConnectionTag.isPresent()) {
176+
document = (Xml.Document)
177+
new ChangeTagValueVisitor<>(devConnectionTag.get(), "scm:git:git@github.com:${gitHubRepo}.git")
178+
.visitNonNull(document, ctx);
179+
// Refresh references after document transformation
180+
root = document.getRoot();
181+
scmTag = root.getChild("scm");
182+
}
183+
184+
// Update url
185+
Optional<Xml.Tag> scmUrlTag = scmTag.isPresent() ? scmTag.get().getChild("url") : Optional.empty();
186+
if (scmUrlTag.isPresent()) {
187+
document = (Xml.Document) new ChangeTagValueVisitor<>(scmUrlTag.get(), "https://github.com/${gitHubRepo}")
188+
.visitNonNull(document, ctx);
189+
}
190+
191+
return document;
192+
}
193+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@import io.jenkins.tools.pluginmodernizer.core.model.Plugin
2+
@import io.jenkins.tools.pluginmodernizer.core.model.Recipe
3+
@param Plugin plugin
4+
@param Recipe recipe
5+
Hello `${plugin.getName()}` developers! :wave:
6+
7+
This is an automated pull request created by the [Jenkins Plugin Modernizer](https://github.com/jenkins-infra/plugin-modernizer-tool) tool. The tool has applied the following recipe to modernize the plugin:
8+
<details aria-label="Recipe details for ${recipe.getDisplayName()}">
9+
<summary>${recipe.getDisplayName()}</summary>
10+
<p><em>${recipe.getName()}</em></p>
11+
<blockquote>${recipe.getDescription()}</blockquote>
12+
</details>
13+
14+
## Enable incrementals
15+
16+
Jenkins evaluation of pull request builds is faster and easier when incremental builds are enabled. See the jenkins.io [enable incrementals tutorial](https://www.jenkins.io/doc/developer/tutorial-improve/enable-incrementals/) for more details.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@import io.jenkins.tools.pluginmodernizer.core.model.Plugin
2+
@import io.jenkins.tools.pluginmodernizer.core.model.Recipe
3+
@param Plugin plugin
4+
@param Recipe recipe
5+
chore: Enable incrementals

0 commit comments

Comments
 (0)