From 226b8302565076ece99e2a42cb1e7ec2ce50469b Mon Sep 17 00:00:00 2001 From: Guillaume Le Floch Date: Thu, 29 Apr 2021 11:01:25 +0200 Subject: [PATCH 1/6] Add support for test/restest command in pull request comment --- .../bot/PullRequestCommandHandler.java | 79 +++++++++++++++++++ .../java/io/quarkus/bot/command/Command.java | 14 ++++ .../bot/command/RerunWorkflowCommand.java | 65 +++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 src/main/java/io/quarkus/bot/PullRequestCommandHandler.java create mode 100644 src/main/java/io/quarkus/bot/command/Command.java create mode 100644 src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java diff --git a/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java new file mode 100644 index 0000000..9d6bf10 --- /dev/null +++ b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java @@ -0,0 +1,79 @@ +package io.quarkus.bot; + +import io.quarkiverse.githubapp.event.IssueComment; +import io.quarkus.bot.command.Command; +import io.quarkus.bot.config.QuarkusBotConfig; +import org.jboss.logging.Logger; +import org.kohsuke.github.GHEventPayload; +import org.kohsuke.github.GHIssue; +import org.kohsuke.github.GHPermissionType; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHUser; +import org.kohsuke.github.ReactionContent; + +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import java.io.IOException; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PullRequestCommandHandler { + + private static final Logger LOG = Logger.getLogger(PullRequestCommandHandler.class); + + private static final String QUARKUS_BOT_NAME = "quarkus-bot[bot]"; + private static final Pattern QUARKUS_BOT_MENTION = Pattern.compile("^@(?:quarkus-?)?bot\\s+([a-z _\\-]+)"); + + @Inject + Instance> commands; + + @Inject + QuarkusBotConfig quarkusBotConfig; + + public void onComment(@IssueComment.Created @IssueComment.Edited GHEventPayload.IssueComment commentPayload) + throws IOException { + GHUser user = commentPayload.getComment().getUser(); + GHIssue issue = commentPayload.getIssue(); + GHRepository repository = commentPayload.getRepository(); + + if (QUARKUS_BOT_NAME.equals(commentPayload.getComment().getUserName())) { + return; + } + + if (issue.isPullRequest()) { + Optional> command = extractCommand(commentPayload.getComment().getBody()); + if (command.isPresent() && canRunCommand(repository, user)) { + GHPullRequest pullRequest = repository.getPullRequest(issue.getNumber()); + ReactionContent reactionResult = command.get().run(pullRequest); + postReaction(commentPayload, issue, reactionResult); + } else { + postReaction(commentPayload, issue, ReactionContent.MINUS_ONE); + } + } + } + + private void postReaction(GHEventPayload.IssueComment comment, GHIssue issue, ReactionContent reactionResult) + throws IOException { + if (!quarkusBotConfig.isDryRun()) { + comment.getComment().createReaction(reactionResult); + } else { + LOG.info("Pull Request #" + issue.getNumber() + " - Add reaction: " + reactionResult.getContent()); + } + } + + private Optional> extractCommand(String comment) { + Matcher matcher = QUARKUS_BOT_MENTION.matcher(comment); + if (matcher.matches()) { + String commandLabel = matcher.group(1); + return commands.stream().filter(command -> command.labels().contains(commandLabel)).findFirst(); + } + return Optional.empty(); + } + + private boolean canRunCommand(GHRepository repository, GHUser user) throws IOException { + return repository.getPermission(user) == GHPermissionType.WRITE + || repository.getPermission(user) == GHPermissionType.ADMIN; + } +} diff --git a/src/main/java/io/quarkus/bot/command/Command.java b/src/main/java/io/quarkus/bot/command/Command.java new file mode 100644 index 0000000..de70251 --- /dev/null +++ b/src/main/java/io/quarkus/bot/command/Command.java @@ -0,0 +1,14 @@ +package io.quarkus.bot.command; + +import org.kohsuke.github.ReactionContent; + +import java.io.IOException; +import java.util.List; + +public interface Command { + + List labels(); + + ReactionContent run(T input) throws IOException; + +} diff --git a/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java b/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java new file mode 100644 index 0000000..4a23f91 --- /dev/null +++ b/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java @@ -0,0 +1,65 @@ +package io.quarkus.bot.command; + +import io.quarkus.bot.config.QuarkusBotConfig; +import io.quarkus.bot.workflow.WorkflowConstants; +import org.jboss.logging.Logger; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHWorkflowRun; +import org.kohsuke.github.ReactionContent; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@ApplicationScoped +public class RerunWorkflowCommand implements Command { + + private static final Logger LOG = Logger.getLogger(RerunWorkflowCommand.class); + + @Inject + QuarkusBotConfig quarkusBotConfig; + + @Override + public List labels() { + return Arrays.asList("test", "retest"); + } + + @Override + public ReactionContent run(GHPullRequest pullRequest) throws IOException { + GHRepository repository = pullRequest.getRepository(); + + List ghWorkflowRuns = repository + .queryWorkflowRuns() + .branch(pullRequest.getHead().getRef()) + .status(GHWorkflowRun.Status.COMPLETED) + .list().toList(); + + Map> lastWorkflowRuns = ghWorkflowRuns.stream() + .filter(workflowRun -> WorkflowConstants.QUARKUS_CI_WORKFLOW_NAME.equals(workflowRun.getName()) + || WorkflowConstants.QUARKUS_DOCUMENTATION_CI_WORKFLOW_NAME.equals(workflowRun.getName())) + .filter(workflowRun -> workflowRun.getHeadRepository().getOwnerName() + .equals(pullRequest.getHead().getRepository().getOwnerName())) + .collect(Collectors.groupingBy(GHWorkflowRun::getName, + Collectors.maxBy(Comparator.comparing(GHWorkflowRun::getRunNumber)))); + + for (Map.Entry> lastWorkflowRun : lastWorkflowRuns.entrySet()) { + if (lastWorkflowRun.getValue().isPresent()) { + if (!quarkusBotConfig.isDryRun()) { + lastWorkflowRun.getValue().get().rerun(); + LOG.debug("Pull request #" + pullRequest.getNumber() + " - Restart workflow: " + + lastWorkflowRun.getValue().get().getHtmlUrl()); + } else { + LOG.info("Pull request #" + pullRequest.getNumber() + " - Restart workflow " + lastWorkflowRun.getKey()); + } + } + } + return ReactionContent.ROCKET; + } +} From 563025080a98eedd09c845e3d2043ed4e8df2db2 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 7 May 2021 09:43:31 +0200 Subject: [PATCH 2/6] Simplify logic of PullRequestCommandHandler In passing, only post a reaction if a command has been detected. --- .../bot/PullRequestCommandHandler.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java index 9d6bf10..f9531e0 100644 --- a/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java +++ b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java @@ -32,6 +32,7 @@ public class PullRequestCommandHandler { @Inject QuarkusBotConfig quarkusBotConfig; + @SuppressWarnings("deprecation") public void onComment(@IssueComment.Created @IssueComment.Edited GHEventPayload.IssueComment commentPayload) throws IOException { GHUser user = commentPayload.getComment().getUser(); @@ -42,18 +43,25 @@ public void onComment(@IssueComment.Created @IssueComment.Edited GHEventPayload. return; } - if (issue.isPullRequest()) { - Optional> command = extractCommand(commentPayload.getComment().getBody()); - if (command.isPresent() && canRunCommand(repository, user)) { - GHPullRequest pullRequest = repository.getPullRequest(issue.getNumber()); - ReactionContent reactionResult = command.get().run(pullRequest); - postReaction(commentPayload, issue, reactionResult); - } else { - postReaction(commentPayload, issue, ReactionContent.MINUS_ONE); - } + if (!issue.isPullRequest()) { + return; + } + + Optional> command = extractCommand(commentPayload.getComment().getBody()); + if (command.isEmpty()) { + return; + } + + if (canRunCommand(repository, user)) { + GHPullRequest pullRequest = repository.getPullRequest(issue.getNumber()); + ReactionContent reactionResult = command.get().run(pullRequest); + postReaction(commentPayload, issue, reactionResult); + } else { + postReaction(commentPayload, issue, ReactionContent.MINUS_ONE); } } + @SuppressWarnings("deprecation") private void postReaction(GHEventPayload.IssueComment comment, GHIssue issue, ReactionContent reactionResult) throws IOException { if (!quarkusBotConfig.isDryRun()) { From 03289dd43fb497f5b882d639695424fc69d914fd Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 7 May 2021 09:44:33 +0200 Subject: [PATCH 3/6] Only return rocket reaction if a workflow run is restarted --- .../bot/command/RerunWorkflowCommand.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java b/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java index 4a23f91..8a64fcd 100644 --- a/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java +++ b/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java @@ -49,17 +49,23 @@ public ReactionContent run(GHPullRequest pullRequest) throws IOException { .collect(Collectors.groupingBy(GHWorkflowRun::getName, Collectors.maxBy(Comparator.comparing(GHWorkflowRun::getRunNumber)))); - for (Map.Entry> lastWorkflowRun : lastWorkflowRuns.entrySet()) { - if (lastWorkflowRun.getValue().isPresent()) { + boolean workflowRunRestarted = false; + + for (Map.Entry> lastWorkflowRunEntry : lastWorkflowRuns.entrySet()) { + if (lastWorkflowRunEntry.getValue().isPresent()) { + GHWorkflowRun lastWorkflowRun = lastWorkflowRunEntry.getValue().get(); if (!quarkusBotConfig.isDryRun()) { - lastWorkflowRun.getValue().get().rerun(); + lastWorkflowRun.rerun(); + workflowRunRestarted = true; LOG.debug("Pull request #" + pullRequest.getNumber() + " - Restart workflow: " - + lastWorkflowRun.getValue().get().getHtmlUrl()); + + lastWorkflowRun.getName() + " - " + lastWorkflowRun.getId()); } else { - LOG.info("Pull request #" + pullRequest.getNumber() + " - Restart workflow " + lastWorkflowRun.getKey()); + LOG.info("Pull request #" + pullRequest.getNumber() + " - Restart workflow " + + lastWorkflowRun.getName() + " - " + lastWorkflowRun.getId()); } } } - return ReactionContent.ROCKET; + + return workflowRunRestarted ? ReactionContent.ROCKET : ReactionContent.CONFUSED; } } From 676a8a93db5424610a94045b6196e22db072353f Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 7 May 2021 09:45:27 +0200 Subject: [PATCH 4/6] Normalize the executed command --- src/main/java/io/quarkus/bot/PullRequestCommandHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java index f9531e0..3d7afe4 100644 --- a/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java +++ b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java @@ -15,6 +15,7 @@ import javax.enterprise.inject.Instance; import javax.inject.Inject; import java.io.IOException; +import java.util.Locale; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -74,7 +75,7 @@ private void postReaction(GHEventPayload.IssueComment comment, GHIssue issue, Re private Optional> extractCommand(String comment) { Matcher matcher = QUARKUS_BOT_MENTION.matcher(comment); if (matcher.matches()) { - String commandLabel = matcher.group(1); + String commandLabel = matcher.group(1).toLowerCase(Locale.ROOT).trim(); return commands.stream().filter(command -> command.labels().contains(commandLabel)).findFirst(); } return Optional.empty(); From 4e8b79c09f524cdff1eb47a2c90c7eedb308ac7d Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 7 May 2021 09:46:25 +0200 Subject: [PATCH 5/6] Only call getPermission() once to limit the number of API calls --- src/main/java/io/quarkus/bot/PullRequestCommandHandler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java index 3d7afe4..5652d3b 100644 --- a/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java +++ b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java @@ -82,7 +82,8 @@ private Optional> extractCommand(String comment) { } private boolean canRunCommand(GHRepository repository, GHUser user) throws IOException { - return repository.getPermission(user) == GHPermissionType.WRITE - || repository.getPermission(user) == GHPermissionType.ADMIN; + GHPermissionType permission = repository.getPermission(user); + + return permission == GHPermissionType.WRITE || permission == GHPermissionType.ADMIN; } } From f485632b934c65e8355d975d9546e9cb96469adb Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 10 May 2021 19:58:01 +0200 Subject: [PATCH 6/6] Use a personal access token to trigger the workflow rerun There is a bug in the GitHub API and for now it's not possible to trigger a rerun with an installation token. We have no ETA for the fix so using a personal access token is the only workaround for now. --- .../bot/command/RerunWorkflowCommand.java | 53 +++++++++++++++---- .../quarkus/bot/config/QuarkusBotConfig.java | 10 ++++ 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java b/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java index 8a64fcd..da1e9b3 100644 --- a/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java +++ b/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java @@ -1,15 +1,5 @@ package io.quarkus.bot.command; -import io.quarkus.bot.config.QuarkusBotConfig; -import io.quarkus.bot.workflow.WorkflowConstants; -import org.jboss.logging.Logger; -import org.kohsuke.github.GHPullRequest; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GHWorkflowRun; -import org.kohsuke.github.ReactionContent; - -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; @@ -18,6 +8,23 @@ import java.util.Optional; import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.jboss.logging.Logger; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHWorkflowRun; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.kohsuke.github.ReactionContent; +import org.kohsuke.github.extras.okhttp3.OkHttpConnector; + +import io.quarkus.bot.config.QuarkusBotConfig; +import io.quarkus.bot.workflow.WorkflowConstants; +import okhttp3.OkHttpClient; + @ApplicationScoped public class RerunWorkflowCommand implements Command { @@ -26,6 +33,19 @@ public class RerunWorkflowCommand implements Command { @Inject QuarkusBotConfig quarkusBotConfig; + @Inject + OkHttpClient okHttpClient; + + private GitHub gitHub; + + @PostConstruct + public void initGitHubClient() throws IOException { + if (quarkusBotConfig.getAccessToken().isPresent()) { + gitHub = new GitHubBuilder().withOAuthToken(quarkusBotConfig.getAccessToken().get()) + .withConnector(new OkHttpConnector(okHttpClient)).build(); + } + } + @Override public List labels() { return Arrays.asList("test", "retest"); @@ -33,6 +53,12 @@ public List labels() { @Override public ReactionContent run(GHPullRequest pullRequest) throws IOException { + if (gitHub == null) { + LOG.error("Pull request #" + pullRequest.getNumber() + + " - Unable to restart workflow as no access token was provided in the config"); + return ReactionContent.MINUS_ONE; + } + GHRepository repository = pullRequest.getRepository(); List ghWorkflowRuns = repository @@ -54,8 +80,13 @@ public ReactionContent run(GHPullRequest pullRequest) throws IOException { for (Map.Entry> lastWorkflowRunEntry : lastWorkflowRuns.entrySet()) { if (lastWorkflowRunEntry.getValue().isPresent()) { GHWorkflowRun lastWorkflowRun = lastWorkflowRunEntry.getValue().get(); + + // There is a bug in the GitHub API and we have to use a personal access token to execute the rerun() call + GHRepository accessTokenRepository = gitHub.getRepository(lastWorkflowRun.getRepository().getFullName()); + GHWorkflowRun accessTokenLastWorkflowRun = accessTokenRepository.getWorkflowRun(lastWorkflowRun.getId()); + if (!quarkusBotConfig.isDryRun()) { - lastWorkflowRun.rerun(); + accessTokenLastWorkflowRun.rerun(); workflowRunRestarted = true; LOG.debug("Pull request #" + pullRequest.getNumber() + " - Restart workflow: " + lastWorkflowRun.getName() + " - " + lastWorkflowRun.getId()); diff --git a/src/main/java/io/quarkus/bot/config/QuarkusBotConfig.java b/src/main/java/io/quarkus/bot/config/QuarkusBotConfig.java index afa4f42..73cab49 100644 --- a/src/main/java/io/quarkus/bot/config/QuarkusBotConfig.java +++ b/src/main/java/io/quarkus/bot/config/QuarkusBotConfig.java @@ -9,6 +9,8 @@ public class QuarkusBotConfig { Optional dryRun; + Optional accessToken; + public void setDryRun(Optional dryRun) { this.dryRun = dryRun; } @@ -16,4 +18,12 @@ public void setDryRun(Optional dryRun) { public boolean isDryRun() { return dryRun.isPresent() && dryRun.get(); } + + public void setAccessToken(Optional accessToken) { + this.accessToken = accessToken; + } + + public Optional getAccessToken() { + return accessToken; + } }