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} +