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..5652d3b --- /dev/null +++ b/src/main/java/io/quarkus/bot/PullRequestCommandHandler.java @@ -0,0 +1,89 @@ +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.Locale; +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; + + @SuppressWarnings("deprecation") + 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()) { + 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()) { + 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).toLowerCase(Locale.ROOT).trim(); + return commands.stream().filter(command -> command.labels().contains(commandLabel)).findFirst(); + } + return Optional.empty(); + } + + private boolean canRunCommand(GHRepository repository, GHUser user) throws IOException { + GHPermissionType permission = repository.getPermission(user); + + return permission == GHPermissionType.WRITE || permission == 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..da1e9b3 --- /dev/null +++ b/src/main/java/io/quarkus/bot/command/RerunWorkflowCommand.java @@ -0,0 +1,102 @@ +package io.quarkus.bot.command; + +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; + +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 { + + private static final Logger LOG = Logger.getLogger(RerunWorkflowCommand.class); + + @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"); + } + + @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 + .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)))); + + boolean workflowRunRestarted = false; + + 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()) { + accessTokenLastWorkflowRun.rerun(); + workflowRunRestarted = true; + LOG.debug("Pull request #" + pullRequest.getNumber() + " - Restart workflow: " + + lastWorkflowRun.getName() + " - " + lastWorkflowRun.getId()); + } else { + LOG.info("Pull request #" + pullRequest.getNumber() + " - Restart workflow " + + lastWorkflowRun.getName() + " - " + lastWorkflowRun.getId()); + } + } + } + + return workflowRunRestarted ? ReactionContent.ROCKET : ReactionContent.CONFUSED; + } +} 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; + } }