Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subscriber for check run event in order to handle rerun request #25

Merged
merged 14 commits into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<github-api.version>1.115</github-api.version>
<github-branch-source.version>2.8.3</github-branch-source.version>
<github-plugin.version>1.31.0</github-plugin.version>
<branch-api.version>2.5.8</branch-api.version>

<!-- Test Library Dependencies Versions -->
<jetty.version>9.4.31.v20200723</jetty.version>
Expand Down Expand Up @@ -91,6 +92,11 @@
<artifactId>github</artifactId>
<version>${github-plugin.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>branch-api</artifactId>
<version>${branch-api.version}</version>
</dependency>

<!-- Test Dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package io.jenkins.plugins.checks.github;

import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.model.*;
import hudson.security.ACL;
import hudson.security.ACLContext;
import io.jenkins.plugins.util.JenkinsFacade;
import jenkins.model.ParameterizedJobMixIn;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.jenkinsci.plugins.github.extension.GHSubscriberEvent;
import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource;
import org.kohsuke.github.*;

import java.io.IOException;
import java.io.StringReader;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
* This subscriber manages {@link GHEvent#CHECK_RUN} event and handles the re-run action request.
*/
@Extension
public class CheckRunGHEventSubscriber extends GHEventsSubscriber {
private static final Logger LOGGER = Logger.getLogger(CheckRunGHEventSubscriber.class.getName());

private final JenkinsFacade jenkinsFacade;
private final GitHubSCMFacade gitHubSCMFacade;

/**
* Construct the subscriber.
*/
public CheckRunGHEventSubscriber() {
this(new JenkinsFacade(), new GitHubSCMFacade());
}

@VisibleForTesting
CheckRunGHEventSubscriber(final JenkinsFacade jenkinsFacade, final GitHubSCMFacade gitHubSCMFacade) {
super();

this.jenkinsFacade = jenkinsFacade;
this.gitHubSCMFacade = gitHubSCMFacade;
}

@Override
protected boolean isApplicable(@Nullable final Item item) {
if (item instanceof Job<?, ?>) {
return gitHubSCMFacade.findGitHubSCMSource((Job<?, ?>)item).isPresent();
}

return false;
}

@Override
protected Set<GHEvent> events() {
return Collections.unmodifiableSet(new HashSet<>(Collections.singletonList(GHEvent.CHECK_RUN)));
}

@Override
protected void onEvent(final GHSubscriberEvent event) {
// TODO: open a PR in Checks API to expose properties in GHRequestedAction
final String payload = event.getPayload();
JSONObject json = JSONObject.fromObject(payload);
if (!json.getString("action").equals("requested_action")
|| !json.getJSONObject("requested_action").get("identifier").equals("rerun")) {
LOGGER.log(Level.FINE, "Unsupported check run event: " + payload.replaceAll("[\r\n]", ""));
return;
}

GHEventPayload.CheckRun checkRun;
try {
checkRun = GitHub.offline().parseEventPayload(new StringReader(payload), GHEventPayload.CheckRun.class);
}
catch (IOException e) {
throw new IllegalStateException("Could not parse check run event: " + payload.replaceAll("\r\n", ""), e);
}

LOGGER.log(Level.INFO, "Received rerun request through GitHub checks API.");
try (ACLContext ignored = ACL.as(ACL.SYSTEM)) {
scheduleRerun(checkRun, payload);
}
}

private void scheduleRerun(final GHEventPayload.CheckRun checkRun, final String payload) {
final GHRepository repository = checkRun.getRepository();
final String branchName = getBranchName(JSONObject.fromObject(payload));

for (Job<?, ?> job : jenkinsFacade.getAllJobs()) {
XiongKezhi marked this conversation as resolved.
Show resolved Hide resolved
Optional<GitHubSCMSource> source = gitHubSCMFacade.findGitHubSCMSource(job);

if (source.isPresent() && source.get().getRepoOwner().equals(repository.getOwnerName())
&& source.get().getRepository().equals(repository.getName())
&& job.getName().equals(branchName)) {
Cause cause = new GitHubChecksRerunActionCause(checkRun.getSender().getLogin());
ParameterizedJobMixIn.scheduleBuild2(job, 0, new CauseAction(cause));

LOGGER.log(Level.INFO, String.format("Scheduled rerun (build #%d) for job %s, requested by %s",
job.getNextBuildNumber(), jenkinsFacade.getFullNameOf(job),
checkRun.getSender().getLogin()).replaceAll("[\r\n]", ""));
return;
}
}

LOGGER.log(Level.WARNING, String.format("No proper job found for the rerun request from repository: %s and "
+ "branch: %s", repository.getFullName(), branchName).replaceAll("\r\n", ""));
}

/**
* Get branch name from {@link JSONObject}.
*
* This method will be replaced by {@link CheckRunGHEventSubscriber#getBranchName(GHEventPayload.CheckRun, String)}
* after the release of github--api-plugin 1.116. The github-api has already released 1.116 and make
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so do we need this method? seems like it should be available?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method is available, but using the object from github-api is better since we don't have to parse the json again, and more safe.

the github-api has released 1.116, may just need to wait two or three days, the github-api-plugin will follow up

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kk

* {@code getPullRequests} method public, see https://github.com/hub4j/github-api/pull/909.
*
* @param json
* json object from check run payload
* @return name of the branch to be scheduled
*/
private String getBranchName(final JSONObject json) {
String branchName = "master";
JSONArray pullRequests = json.getJSONObject("check_run").getJSONArray("pull_requests");
if (!pullRequests.isEmpty()) {
branchName = "PR-" + pullRequests.getJSONObject(0).getString("number");
}

return branchName;
}

@SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD")
@SuppressWarnings({"PMD.UnusedPrivateMethod", "PMD.UnusedFormalParameter"})
private String getBranchName(final GHEventPayload.CheckRun checkRun, final String payload) {
// String branchName = "master";
// try {
// List<GHPullRequest> pullRequests = checkRun.getCheckRun().getPullRequests();
// if (!pullRequests.isEmpty()) {
// branchName = "PR" + pullRequests.get(0).getNumber();
// }
// }
// catch (IOException e) {
// throw new IllegalStateException("Could not get pull request participated in rerun request: "
// + payload.replaceAll("\r\n", ""), e);
// }
//
// return branchName;

return StringUtils.EMPTY;
}

/**
* Declares that a build was started due to a user's rerun request through GitHub checks API.
*/
public static class GitHubChecksRerunActionCause extends Cause {
private final String user;

/**
* Construct the cause with user who requested the rerun.
*
* @param user
* name of the user who made the request
*/
public GitHubChecksRerunActionCause(final String user) {
super();

this.user = user;
}

@Override
public String getShortDescription() {
return String.format("Rerun request by %s through GitHub checks API", user);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.jenkins.plugins.checks.github;

import hudson.model.Item;
import hudson.model.Job;
import io.jenkins.plugins.util.JenkinsFacade;
import org.apache.commons.io.FileUtils;
import org.jenkinsci.plugins.github.extension.GHSubscriberEvent;
import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource;
import org.junit.Rule;
import org.junit.jupiter.api.Test;
import org.jvnet.hudson.test.LoggerRule;
import org.kohsuke.github.GHEvent;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Optional;
import java.util.logging.Level;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

class CheckRunGHEventSubscriberTest {
/**
* Rule for the log system.
*/
@Rule
public LoggerRule loggerRule = new LoggerRule();

@Test
void shouldBeApplicableForJobWithGitHubSCMSource() {
Job<?, ?> job = mock(Job.class);
JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class);
GitHubSCMFacade scmFacade = mock(GitHubSCMFacade.class);
GitHubSCMSource source = mock(GitHubSCMSource.class);

when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source));

assertThat(new CheckRunGHEventSubscriber(jenkinsFacade, scmFacade).isApplicable(job))
.isTrue();
}

@Test
void shouldNotBeApplicableForJobWithoutGitHubSCMSource() {
Job<?, ?> job = mock(Job.class);
assertThat(new CheckRunGHEventSubscriber().isApplicable(job))
.isFalse();
}

@Test
void shouldNotBeApplicableForItemThatNotInstanceOfJob() {
Item item = mock(Item.class);
assertThat(new CheckRunGHEventSubscriber().isApplicable(item))
.isFalse();
}

@Test
void shouldSubscribeToCheckRunEvent() {
assertThat(new CheckRunGHEventSubscriber().events()).containsOnly(GHEvent.CHECK_RUN);
}

@Test
void shouldProcessCheckRunEventWithRerunAction() throws IOException {
loggerRule.record(CheckRunGHEventSubscriber.class.getName(), Level.INFO).capture(1);
new CheckRunGHEventSubscriber(mock(JenkinsFacade.class), mock(GitHubSCMFacade.class))
.onEvent(new GHSubscriberEvent("shouldScheduleBuildIfRerunRequested", GHEvent.CHECK_RUN,
FileUtils.readFileToString(new File(getClass().getResource(getClass().getSimpleName()
+ "/check-run-event-with-rerun-action.json").getFile()),
StandardCharsets.UTF_8)));
assertThat(loggerRule.getMessages().get(0)).contains("Received rerun request through GitHub checks API.");
}

@Test
void shouldIgnoreCheckRunEventWithoutRequestedAction() {
GHSubscriberEvent event = mock(GHSubscriberEvent.class);
when(event.getPayload()).thenReturn("{\"action\":\"created\"}");

loggerRule.record(CheckRunGHEventSubscriber.class.getName(), Level.FINE).capture(1);
new CheckRunGHEventSubscriber(mock(JenkinsFacade.class), mock(GitHubSCMFacade.class)).onEvent(event);
assertThat(loggerRule.getMessages()).contains("Unsupported check run event: {\"action\":\"created\"}");
}

@Test
void shouldScheduleRerunWhenFindCorrespondingJob() throws IOException {
Job<?, ?> job = mock(Job.class);
JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class);
GitHubSCMFacade scmFacade = mock(GitHubSCMFacade.class);
GitHubSCMSource source = mock(GitHubSCMSource.class);

when(jenkinsFacade.getAllJobs()).thenReturn(Collections.singletonList(job));
when(jenkinsFacade.getFullNameOf(job)).thenReturn("Sandbox/PR-1");
when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source));
when(source.getRepoOwner()).thenReturn("XiongKezhi");
when(source.getRepository()).thenReturn("Sandbox");
when(job.getNextBuildNumber()).thenReturn(1);
when(job.getName()).thenReturn("PR-1");

loggerRule.record(CheckRunGHEventSubscriber.class.getName(), Level.INFO).capture(1);
new CheckRunGHEventSubscriber(jenkinsFacade, scmFacade)
.onEvent(new GHSubscriberEvent("shouldScheduleBuildIfRerunRequested", GHEvent.CHECK_RUN,
FileUtils.readFileToString(new File(getClass().getResource(getClass().getSimpleName()
+ "/check-run-event-with-rerun-action.json").getFile()),
StandardCharsets.UTF_8)));
assertThat(loggerRule.getMessages())
.contains("Scheduled rerun (build #1) for job Sandbox/PR-1, requested by XiongKezhi");
}

@Test
void shouldNotScheduleRerunWhenNoProperJobFound() throws IOException {
JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class);
when(jenkinsFacade.getAllJobs()).thenReturn(Collections.emptyList());

loggerRule.record(CheckRunGHEventSubscriber.class.getName(), Level.WARNING).capture(1);
new CheckRunGHEventSubscriber(jenkinsFacade, mock(GitHubSCMFacade.class))
.onEvent(new GHSubscriberEvent("shouldScheduleBuildIfRerunRequested", GHEvent.CHECK_RUN,
FileUtils.readFileToString(new File(getClass().getResource(getClass().getSimpleName()
+ "/check-run-event-with-rerun-action.json").getFile()),
StandardCharsets.UTF_8)));
assertThat(loggerRule.getMessages())
.contains("No proper job found for the rerun request from repository: XiongKezhi/Sandbox and "
+ "branch: PR-1");
}

@Test
void shouldContainsUserInShortDescriptionOfGitHubChecksRerunActionCause() {
CheckRunGHEventSubscriber.GitHubChecksRerunActionCause cause =
new CheckRunGHEventSubscriber.GitHubChecksRerunActionCause("jenkins");

assertThat(cause.getShortDescription()).isEqualTo("Rerun request by jenkins through GitHub checks API");
}
}
Loading