Skip to content

Commit c4a56b2

Browse files
authored
Merge pull request jenkins-infra#1459 from PratikMane0112/feature/enhance-addcodeowner-recipe
Better support for multi-module plugins
2 parents 82d65e2 + 269b9b7 commit c4a56b2

11 files changed

Lines changed: 893 additions & 11 deletions

File tree

plugin-modernizer-cli/src/main/java/io/jenkins/tools/pluginmodernizer/cli/converter/PluginPathConverter.java

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,96 @@
22

33
import io.jenkins.tools.pluginmodernizer.core.model.Plugin;
44
import io.jenkins.tools.pluginmodernizer.core.utils.StaticPomParser;
5+
import java.io.IOException;
56
import java.nio.file.Files;
67
import java.nio.file.Path;
8+
import java.util.stream.Stream;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
711
import picocli.CommandLine;
812

913
/**
1014
* Custom converter to get a list of plugin from a local folder
1115
*/
1216
public class PluginPathConverter implements CommandLine.ITypeConverter<Plugin> {
1317

18+
private static final Logger LOG = LoggerFactory.getLogger(PluginPathConverter.class);
19+
1420
@Override
1521
public Plugin convert(String value) throws Exception {
1622
Path path = Path.of(value);
1723
if (!Files.isDirectory(path)) {
1824
throw new IllegalArgumentException("Path is not a directory: " + path);
1925
}
20-
// We assume the plugin is at the pom, will not work for multi module projects
26+
2127
Path pom = path.resolve("pom.xml");
2228
if (!Files.exists(pom)) {
2329
throw new IllegalArgumentException("Path does not contain a pom.xml: " + path);
2430
}
25-
StaticPomParser staticPomParser = new StaticPomParser(pom.toString());
26-
String packaging = staticPomParser.getPackaging();
27-
if (!"hpi".equals(packaging)) {
28-
throw new IllegalArgumentException("Path does not contain a Jenkins plugin: " + path);
31+
32+
StaticPomParser rootPomParser = new StaticPomParser(pom.toString());
33+
String packaging = rootPomParser.getPackaging();
34+
35+
// Check if this is a single-module Jenkins plugin
36+
if ("hpi".equals(packaging)) {
37+
String artifactId = rootPomParser.getArtifactId();
38+
if (artifactId == null) {
39+
throw new IllegalArgumentException("Path does not contain a valid Jenkins plugin: " + path);
40+
}
41+
LOG.info("Found single-module plugin '{}' at root level", artifactId);
42+
return Plugin.build(artifactId, path);
2943
}
30-
String artifactId = staticPomParser.getArtifactId();
31-
if (artifactId == null) {
32-
throw new IllegalArgumentException("Path does not contain a valid Jenkins plugin: " + path);
44+
45+
// Check if this is a multi-module project (packaging = pom)
46+
if ("pom".equals(packaging) || packaging == null || packaging.isEmpty()) {
47+
LOG.info("Detected multi-module project, searching for Jenkins plugin module...");
48+
Path pluginPath = findJenkinsPluginModule(path);
49+
if (pluginPath != null) {
50+
StaticPomParser pluginPomParser =
51+
new StaticPomParser(pluginPath.resolve("pom.xml").toString());
52+
String artifactId = pluginPomParser.getArtifactId();
53+
if (artifactId == null) {
54+
throw new IllegalArgumentException(
55+
"Plugin module does not contain valid artifactId: " + pluginPath);
56+
}
57+
LOG.info("Found Jenkins plugin module '{}' at: {}", artifactId, pluginPath);
58+
return Plugin.build(artifactId, pluginPath);
59+
}
60+
throw new IllegalArgumentException(
61+
"Multi-module project detected but no module with packaging 'hpi' found" + path);
62+
}
63+
64+
throw new IllegalArgumentException(
65+
"Path does not contain a Jenkins plugin (packaging must be 'hpi' or a multi-module project with an hpi module): "
66+
+ path);
67+
}
68+
69+
/**
70+
* Find the Jenkins plugin module in a multi-module project.
71+
* Searches all subdirectories for a pom.xml with packaging 'hpi'.
72+
*
73+
* @param rootPath The root path of the multi-module project
74+
* @return The path to the plugin module, or null if not found
75+
* @throws IOException if an I/O error occurs
76+
*/
77+
private Path findJenkinsPluginModule(Path rootPath) throws IOException {
78+
try (Stream<Path> paths = Files.walk(rootPath, 2)) { // Search up to 2 levels deep
79+
return paths.filter(Files::isDirectory)
80+
.filter(dir -> !dir.equals(rootPath)) // Skip root directory
81+
.filter(dir -> Files.exists(dir.resolve("pom.xml")))
82+
.filter(dir -> {
83+
try {
84+
StaticPomParser parser =
85+
new StaticPomParser(dir.resolve("pom.xml").toString());
86+
String packaging = parser.getPackaging();
87+
return "hpi".equals(packaging);
88+
} catch (Exception e) {
89+
LOG.debug("Failed to parse pom.xml in {}: {}", dir, e.getMessage());
90+
return false;
91+
}
92+
})
93+
.findFirst()
94+
.orElse(null);
3395
}
34-
// Build a local plugin
35-
return Plugin.build(artifactId, path);
3696
}
3797
}

plugin-modernizer-cli/src/test/java/io/jenkins/tools/pluginmodernizer/cli/CommandLineITCase.java

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.security.KeyPairGenerator;
2323
import java.security.PrivateKey;
2424
import java.security.Security;
25+
import java.util.List;
2526
import java.util.Properties;
2627
import java.util.stream.Stream;
2728
import org.apache.commons.io.FileUtils;
@@ -645,6 +646,129 @@ public void testRecipeOnLocalPluginWithRunMode(WireMockRuntimeInfo wmRuntimeInfo
645646
}
646647
}
647648

649+
@Test
650+
@Tag("Slow")
651+
public void testMultiModulePluginAddCodeOwner(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
652+
653+
Path logFile = setupLogs("testMultiModulePluginAddCodeOwner");
654+
655+
// Copy multi-module plugin to cache and use as local plugin
656+
final String plugin = "test-plugin";
657+
final Path multiModulePluginPath = Path.of("src/test/resources").resolve("multi-module-plugin");
658+
Path targetPath = cachePath
659+
.resolve("jenkins-plugin-modernizer-cli")
660+
.resolve(plugin)
661+
.resolve("sources");
662+
FileUtils.copyDirectory(multiModulePluginPath.toFile(), targetPath.toFile());
663+
Git.init().setDirectory(targetPath.toFile()).call().close();
664+
665+
final String recipe = "AddCodeOwner";
666+
667+
try (GitHubServerContainer gitRemote = new GitHubServerContainer(wmRuntimeInfo, keysPath, plugin, "main")) {
668+
669+
gitRemote.start();
670+
671+
// Junit attachment with logs file for the plugin build
672+
System.out.printf("[[ATTACHMENT|%s]]%n", getMavenInvokerLog(plugin));
673+
System.out.printf("[[ATTACHMENT|%s]]%n", logFile.toAbsolutePath());
674+
675+
Invoker invoker = buildInvoker();
676+
InvocationRequest request = buildRequest(
677+
"run --recipe %s %s".formatted(recipe, getRunArgs(wmRuntimeInfo, Plugin.build(plugin, targetPath))),
678+
logFile);
679+
InvocationResult result = invoker.execute(request);
680+
681+
// Assert output
682+
assertAll(
683+
() -> assertEquals(0, result.getExitCode()),
684+
() -> assertTrue(
685+
Files.readAllLines(logFile).stream()
686+
.anyMatch(line -> line.matches("(.*)Modified file: .github/CODEOWNERS(.*)")),
687+
"Code owner file not modified on logs"),
688+
() -> assertTrue(Files.readAllLines(logFile).stream()
689+
.anyMatch(line -> line.matches("(.*)Dry run mode. Changes were made on (.*)"))));
690+
691+
// Check that CODEOWNERS file was created in the plugin module subdirectory
692+
Path codeownersPath = targetPath.resolve("test-plugin").resolve(ArchetypeCommonFile.CODEOWNERS.getPath());
693+
assertTrue(Files.exists(codeownersPath), "Code owner file was not created in plugin module subdirectory");
694+
695+
// Verify correct team name is used (plugin artifactId, not parent artifactId)
696+
List<String> codeownersLines = Files.readAllLines(codeownersPath);
697+
assertTrue(
698+
codeownersLines.stream().anyMatch(line -> line.contains("@jenkinsci/test-plugin-developers")),
699+
"CODEOWNERS file should contain correct team name based on plugin artifactId (test-plugin), not parent artifactId");
700+
assertFalse(
701+
codeownersLines.stream()
702+
.anyMatch(line -> line.contains("@jenkinsci/multi-module-parent-developers")),
703+
"CODEOWNERS file should not contain parent artifactId in team name");
704+
}
705+
}
706+
707+
/**
708+
* Test multi-module plugin with FetchMetadata recipe (requires compilation)
709+
* This verifies that multi-module plugins can be properly compiled and modernized
710+
*/
711+
@Test
712+
@Tag("Slow")
713+
public void testMultiModulePluginFetchMetadata(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
714+
715+
Path logFile = setupLogs("testMultiModulePluginFetchMetadata");
716+
717+
// Copy multi-module plugin to cache and use as local plugin
718+
final String plugin = "test-plugin";
719+
final Path multiModulePluginPath = Path.of("src/test/resources").resolve("multi-module-plugin");
720+
Path targetPath = cachePath
721+
.resolve("jenkins-plugin-modernizer-cli")
722+
.resolve(plugin)
723+
.resolve("sources");
724+
FileUtils.copyDirectory(multiModulePluginPath.toFile(), targetPath.toFile());
725+
Git.init().setDirectory(targetPath.toFile()).call().close();
726+
727+
final String recipe = "FetchMetadata";
728+
729+
try (GitHubServerContainer gitRemote = new GitHubServerContainer(wmRuntimeInfo, keysPath, plugin, "main")) {
730+
731+
gitRemote.start();
732+
733+
// Junit attachment with logs file for the plugin build
734+
System.out.printf("[[ATTACHMENT|%s]]%n", getMavenInvokerLog(plugin));
735+
System.out.printf("[[ATTACHMENT|%s]]%n", logFile.toAbsolutePath());
736+
737+
Invoker invoker = buildInvoker();
738+
InvocationRequest request = buildRequest(
739+
"run --recipe %s %s".formatted(recipe, getRunArgs(wmRuntimeInfo, Plugin.build(plugin, targetPath))),
740+
logFile);
741+
InvocationResult result = invoker.execute(request);
742+
743+
// Assert output - FetchMetadata requires compilation so it should complete successfully
744+
assertAll(
745+
() -> assertEquals(0, result.getExitCode(), "Build should succeed for multi-module plugin"),
746+
() -> assertTrue(
747+
Files.readAllLines(logFile).stream()
748+
.anyMatch(
749+
line -> line.matches("(.*)Multi-module project detected for plugin (.*)")),
750+
"Multi-module detection message not found in logs"),
751+
() -> assertTrue(
752+
Files.readAllLines(logFile).stream()
753+
.anyMatch(line -> line.matches("(.*)Found Jenkins plugin module at:(.*)")),
754+
"Plugin module detection message not found in logs"));
755+
756+
// Verify metadata was collected from the plugin module (not parent)
757+
Path pluginMetadataPath = cachePath
758+
.resolve("jenkins-plugin-modernizer-cli")
759+
.resolve(plugin)
760+
.resolve("plugin-metadata.json");
761+
assertTrue(
762+
Files.exists(pluginMetadataPath), "Plugin metadata file should exist after FetchMetadata recipe");
763+
764+
// Read and verify the metadata contains correct plugin name
765+
String metadataContent = Files.readString(pluginMetadataPath);
766+
assertTrue(
767+
metadataContent.contains("\"pluginName\":\"Test Plugin\""),
768+
"Metadata should contain correct plugin name from plugin module");
769+
}
770+
}
771+
648772
/**
649773
* Build the invoker
650774
* @return the invoker

0 commit comments

Comments
 (0)