diff --git a/plugin-modernizer-core/pom.xml b/plugin-modernizer-core/pom.xml
index a25325ed..13bc4916 100644
--- a/plugin-modernizer-core/pom.xml
+++ b/plugin-modernizer-core/pom.xml
@@ -51,6 +51,14 @@
org.apache.maven
maven-artifact
+
+ org.eclipse.jgit
+ org.eclipse.jgit
+
+
+ org.kohsuke
+ github-api
+
org.openrewrite
diff --git a/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/config/Settings.java b/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/config/Settings.java
index e39272ae..2f709a2a 100644
--- a/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/config/Settings.java
+++ b/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/config/Settings.java
@@ -22,6 +22,14 @@ public class Settings {
public static final String MAVEN_REWRITE_PLUGIN_VERSION;
+ public static final String GITHUB_TOKEN;
+
+ public static final String GITHUB_USERNAME;
+
+ public static final String TEST_PLUGINS_DIRECTORY;
+
+ public static final String ORGANIZATION = "jenkinsci";
+
public static final String RECIPE_DATA_YAML_PATH = "recipe_data.yaml";
public static final ComparableVersion MAVEN_MINIMAL_VERSION = new ComparableVersion("3.9.7");
@@ -40,6 +48,9 @@ public class Settings {
}
DEFAULT_MAVEN_HOME = getDefaultMavenHome();
MAVEN_REWRITE_PLUGIN_VERSION = getRewritePluginVersion();
+ GITHUB_TOKEN = getGithubToken();
+ GITHUB_USERNAME = getGithubUsername();
+ TEST_PLUGINS_DIRECTORY = getTestPluginsDirectory();
}
private static Path getDefaultMavenHome() {
@@ -57,6 +68,18 @@ private static Path getDefaultMavenHome() {
return readProperty("openrewrite.maven.plugin.version", "versions.properties");
}
+ private static String getGithubToken() {
+ return System.getenv("GITHUB_TOKEN");
+ }
+
+ private static String getGithubUsername() {
+ return System.getenv("GITHUB_USERNAME");
+ }
+
+ private static String getTestPluginsDirectory() {
+ return System.getProperty("user.dir") + "/test-plugins/";
+ }
+
/**
* Read a property from a resource file.
* @param key The key to read
diff --git a/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/github/GHService.java b/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/github/GHService.java
new file mode 100644
index 00000000..e13ccc2a
--- /dev/null
+++ b/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/github/GHService.java
@@ -0,0 +1,159 @@
+package io.jenkins.tools.pluginmodernizer.core.github;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Optional;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.jenkins.tools.pluginmodernizer.core.config.Config;
+import io.jenkins.tools.pluginmodernizer.core.config.Settings;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.kohsuke.github.GHIssueState;
+import org.kohsuke.github.GHPullRequest;
+import org.kohsuke.github.GHRepository;
+import org.kohsuke.github.GitHub;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "false positive")
+public class GHService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(GHService.class);
+
+ private final Config config;
+
+ public GHService(Config config) {
+ this.config = config;
+ }
+
+ private static final String GITHUB_TOKEN = Settings.GITHUB_TOKEN;
+ private static final String FORKED_REPO_OWNER = Settings.GITHUB_USERNAME;
+ private static final String ORIGINAL_REPO_OWNER = Settings.ORGANIZATION;
+ // TODO: Change commit message and PR title based on applied recipes
+ private static final String COMMIT_MESSAGE = "Applied transformations with specified recipes";
+ private static final String PR_TITLE = "Automated PR";
+
+ public void forkCloneAndCreateBranch(String pluginName, String branchName) throws IOException, GitAPIException, InterruptedException {
+ Path pluginDirectory = Paths.get(Settings.TEST_PLUGINS_DIRECTORY, pluginName);
+
+ GitHub github = GitHub.connectUsingOAuth(GITHUB_TOKEN);
+ GHRepository originalRepo = github.getRepository(ORIGINAL_REPO_OWNER + "/" + pluginName);
+
+ getOrCreateForkedRepo(github, originalRepo);
+
+ cloneRepositoryIfNeeded(pluginDirectory, pluginName);
+
+ createAndCheckoutBranch(pluginDirectory, branchName);
+ }
+
+ private void getOrCreateForkedRepo(GitHub github, GHRepository originalRepo) throws IOException, InterruptedException {
+ GHRepository forkedRepo = github.getMyself().getRepository(originalRepo.getName());
+ if (forkedRepo == null) {
+ LOG.info("Forking the repository...");
+ originalRepo.fork();
+ Thread.sleep(5000); // Ensure the completion of Fork
+ LOG.info("Repository forked successfully.");
+ } else {
+ LOG.info("Repository already forked.");
+ }
+ }
+
+ private void cloneRepositoryIfNeeded(Path pluginDirectory, String pluginName) throws GitAPIException {
+ if (!Files.exists(pluginDirectory) || !Files.isDirectory(pluginDirectory)) {
+ LOG.info("Cloning {}", pluginName);
+ Git.cloneRepository()
+ .setURI("https://github.com/" + FORKED_REPO_OWNER + "/" + pluginName + ".git")
+ .setDirectory(pluginDirectory.toFile())
+ .call();
+ LOG.info("Cloned successfully.");
+ }
+ }
+
+ private void createAndCheckoutBranch(Path pluginDirectory, String branchName) throws IOException, GitAPIException {
+ try (Git git = Git.open(pluginDirectory.toFile())) {
+ try {
+ git.checkout().setCreateBranch(true).setName(branchName).call();
+ } catch (RefAlreadyExistsException e) {
+ LOG.info("Branch already exists. Checking out the branch.");
+ git.checkout().setName(branchName).call();
+ }
+ }
+ }
+
+ public void commitAndCreatePR(String pluginName, String branchName) throws IOException, GitAPIException {
+ if (config.isDryRun()) {
+ LOG.info("[Dry Run] Skipping commit and pull request creation for {}", pluginName);
+ return;
+ }
+
+ LOG.info("Creating pull request for plugin: {}", pluginName);
+
+ Path pluginDirectory = Paths.get(Settings.TEST_PLUGINS_DIRECTORY, pluginName);
+
+ commitChanges(pluginDirectory);
+
+ pushBranch(pluginDirectory, branchName);
+
+ createPullRequest(pluginName, branchName);
+ }
+
+ private void commitChanges(Path pluginDirectory) throws IOException, GitAPIException {
+ try (Git git = Git.open(pluginDirectory.toFile())) {
+ git.add().addFilepattern(".").call();
+
+ git.commit()
+ .setMessage(COMMIT_MESSAGE)
+ .setSign(false) // Maybe a new option to sign commit?
+ .call();
+
+ LOG.info("Changes committed");
+ }
+ }
+
+ private void pushBranch(Path pluginDirectory, String branchName) throws IOException, GitAPIException {
+ try (Git git = Git.open(pluginDirectory.toFile())) {
+ git.push()
+ .setCredentialsProvider(new UsernamePasswordCredentialsProvider(GITHUB_TOKEN, ""))
+ .setRemote("origin")
+ .setRefSpecs(new RefSpec(branchName + ":" + branchName))
+ .call();
+
+ LOG.info("Pushed changes to forked repository.");
+ }
+ }
+
+ private void createPullRequest(String pluginName, String branchName) throws IOException {
+ GitHub github = GitHub.connectUsingOAuth(GITHUB_TOKEN);
+ GHRepository originalRepo = github.getRepository(ORIGINAL_REPO_OWNER + "/" + pluginName);
+
+ Optional existingPR = checkIfPullRequestExists(originalRepo, branchName);
+
+ if (existingPR.isPresent()) {
+ LOG.info("Pull request already exists: {}", existingPR.get().getHtmlUrl());
+ } else {
+ String prBody = String.format("Applied the following recipes: %s", String.join(", ", config.getRecipes()));
+ GHPullRequest pr = originalRepo.createPullRequest(
+ PR_TITLE,
+ FORKED_REPO_OWNER + ":" + branchName,
+ originalRepo.getDefaultBranch(),
+ prBody
+ );
+
+ LOG.info("Pull request created: {}", pr.getHtmlUrl());
+ }
+ }
+
+ private Optional checkIfPullRequestExists(GHRepository originalRepo, String branchName) throws IOException {
+ List pullRequests = originalRepo.getPullRequests(GHIssueState.OPEN);
+ return pullRequests.stream()
+ .filter(pr -> pr.getHead().getRef().equals(branchName) && pr.getTitle().equals(PR_TITLE))
+ .findFirst();
+ }
+}
diff --git a/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/impl/PluginModernizer.java b/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/impl/PluginModernizer.java
index c2e32f21..2a862523 100644
--- a/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/impl/PluginModernizer.java
+++ b/plugin-modernizer-core/src/main/java/io/jenkins/tools/pluginmodernizer/core/impl/PluginModernizer.java
@@ -3,6 +3,7 @@
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.jenkins.tools.pluginmodernizer.core.config.Config;
import io.jenkins.tools.pluginmodernizer.core.config.Settings;
+import io.jenkins.tools.pluginmodernizer.core.github.GHService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,24 +16,38 @@ public class PluginModernizer {
private final MavenInvoker mavenInvoker;
+ private final GHService ghService;
+
public PluginModernizer(Config config) {
this.config = config;
this.mavenInvoker = new MavenInvoker(config);
+ this.ghService = new GHService(config);
}
public void start() {
- String projectRoot = System.getProperty("user.dir");
LOG.info("Plugins: {}", config.getPlugins());
LOG.info("Recipes: {}", config.getRecipes());
LOG.debug("Cache Path: {}", config.getCachePath());
LOG.debug("Dry Run: {}", config.isDryRun());
LOG.debug("Maven rewrite plugin version: {}", Settings.MAVEN_REWRITE_PLUGIN_VERSION);
for (String plugin : config.getPlugins()) {
- String pluginPath = projectRoot + "/test-plugins/" + plugin;
- LOG.info("Invoking clean phase for plugin: {}", plugin);
- mavenInvoker.invokeGoal(plugin, pluginPath, "clean");
- LOG.info("Invoking rewrite plugin for plugin: {}", plugin);
- mavenInvoker.invokeRewrite(plugin, pluginPath);
+ String pluginPath = Settings.TEST_PLUGINS_DIRECTORY + plugin;
+ String branchName = "apply-transformation-" + plugin;
+
+ try {
+ LOG.info("Forking and cloning {} locally", plugin);
+ ghService.forkCloneAndCreateBranch(plugin, branchName);
+
+ LOG.info("Invoking clean phase for plugin: {}", plugin);
+ mavenInvoker.invokeGoal(plugin, pluginPath, "clean");
+
+ LOG.info("Invoking rewrite plugin for plugin: {}", plugin);
+ mavenInvoker.invokeRewrite(plugin, pluginPath);
+
+ ghService.commitAndCreatePR(plugin, branchName);
+ } catch (Exception e) {
+ LOG.error("Failed to process plugin: {}", plugin, e);
+ }
}
}
diff --git a/pom.xml b/pom.xml
index 2d2e61fc..27c733e4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,8 @@
2.17.2
3.9.8
0.8.12
+ 6.10.0.202406032230-r
+ 1.323
@@ -159,6 +161,16 @@
maven-artifact
${maven.version}
+
+ org.eclipse.jgit
+ org.eclipse.jgit
+ ${jgit.version}
+
+
+ org.kohsuke
+ github-api
+ ${github-api.version}
+