Skip to content

Commit

Permalink
Merge pull request #25 from jenkinsci/rerun-action
Browse files Browse the repository at this point in the history
Add subscriber for check run event in order to handle rerun request
  • Loading branch information
XiongKezhi authored Sep 2, 2020
2 parents f963454 + 5eca631 commit 321521d
Show file tree
Hide file tree
Showing 6 changed files with 1,239 additions and 0 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<github-api.version>1.116</github-api.version>
<github-branch-source.version>2.9.0</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 @@ -102,6 +103,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>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>git</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package io.jenkins.plugins.checks.github;

import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.CheckForNull;
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 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;

/**
* 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 static final String RERUN_ACTION = "rerequested";

private final JenkinsFacade jenkinsFacade;
private final SCMFacade scmFacade;

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

@VisibleForTesting
CheckRunGHEventSubscriber(final JenkinsFacade jenkinsFacade, final SCMFacade scmFacade) {
super();

this.jenkinsFacade = jenkinsFacade;
this.scmFacade = scmFacade;
}

@Override
protected boolean isApplicable(@CheckForNull final Item item) {
if (item instanceof Job<?, ?>) {
return scmFacade.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) {
final String payload = event.getPayload();
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);
}

if (!checkRun.getAction().equals(RERUN_ACTION)) {
LOGGER.log(Level.FINE, "Unsupported check run action: " + checkRun.getAction().replaceAll("[\r\n]", ""));
return;
}

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(checkRun, payload);

for (Job<?, ?> job : jenkinsFacade.getAllJobs()) {
Optional<GitHubSCMSource> source = scmFacade.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]", ""));
}

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

/**
* 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,213 @@
package io.jenkins.plugins.checks.github;

import hudson.model.*;
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 {
static final String RERUN_REQUEST_JSON_FOR_PR = "check-run-event-with-rerun-action-for-pr.json";
static final String RERUN_REQUEST_JSON_FOR_MASTER = "check-run-event-with-rerun-action-for-master.json";

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

@Test
void shouldBeApplicableForJobWithGitHubSCMSource() {
Job<?, ?> job = mock(Job.class);
JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class);
SCMFacade scmFacade = mock(SCMFacade.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 shouldProcessCheckRunEventWithRerequestedAction() throws IOException {
loggerRule.record(CheckRunGHEventSubscriber.class.getName(), Level.INFO).capture(1);
new CheckRunGHEventSubscriber(mock(JenkinsFacade.class), mock(SCMFacade.class))
.onEvent(createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR));
assertThat(loggerRule.getMessages().get(0)).contains("Received rerun request through GitHub checks API.");
}

@Test
void shouldIgnoreCheckRunEventWithoutRerequestedAction() throws IOException {
loggerRule.record(CheckRunGHEventSubscriber.class.getName(), Level.FINE).capture(1);
new CheckRunGHEventSubscriber(mock(JenkinsFacade.class), mock(SCMFacade.class))
.onEvent(createEventWithRerunRequest("check-run-event-with-created-action.json"));
assertThat(loggerRule.getMessages()).contains("Unsupported check run action: created");
}

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

when(jenkinsFacade.getAllJobs()).thenReturn(Collections.singletonList(job));
when(jenkinsFacade.getFullNameOf(job)).thenReturn("codingstyle/PR-1");
when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source));
when(source.getRepoOwner()).thenReturn("XiongKezhi");
when(source.getRepository()).thenReturn("codingstyle");
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(createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR));
assertThat(loggerRule.getMessages())
.contains("Scheduled rerun (build #1) for job codingstyle/PR-1, requested by XiongKezhi");
}

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

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

loggerRule.record(CheckRunGHEventSubscriber.class.getName(), Level.INFO).capture(1);
new CheckRunGHEventSubscriber(jenkinsFacade, scmFacade)
.onEvent(createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_MASTER));
assertThat(loggerRule.getMessages())
.contains("Scheduled rerun (build #1) for job codingstyle/master, requested by XiongKezhi");
}

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

assertNoBuildIsScheduled(jenkinsFacade, mock(SCMFacade.class),
createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR));
}

@Test
void shouldNotScheduleRerunWhenNoSourceForTheJobFound() throws IOException {
Job<?, ?> job = mock(Job.class);
JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class);
SCMFacade scmFacade = mock(SCMFacade.class);

when(jenkinsFacade.getAllJobs()).thenReturn(Collections.singletonList(job));
when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.empty());

assertNoBuildIsScheduled(jenkinsFacade, scmFacade, createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR));
}

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

when(jenkinsFacade.getAllJobs()).thenReturn(Collections.singletonList(job));
when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source));
when(source.getRepoOwner()).thenReturn("jenkinsci");

assertNoBuildIsScheduled(jenkinsFacade, scmFacade, createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR));
}

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

when(jenkinsFacade.getAllJobs()).thenReturn(Collections.singletonList(job));
when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source));
when(source.getRepoOwner()).thenReturn("XiongKezhi");
when(source.getRepository()).thenReturn("github-checks-api");

assertNoBuildIsScheduled(jenkinsFacade, scmFacade, createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR));
}

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

when(jenkinsFacade.getAllJobs()).thenReturn(Collections.singletonList(job));
when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source));
when(source.getRepoOwner()).thenReturn("XiongKezhi");
when(source.getRepository()).thenReturn("codingstyle");
when(job.getName()).thenReturn("PR-2");

assertNoBuildIsScheduled(jenkinsFacade, scmFacade, createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR));
}

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

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

private void assertNoBuildIsScheduled(final JenkinsFacade jenkinsFacade, final SCMFacade scmFacade,
final GHSubscriberEvent event) {
loggerRule.record(CheckRunGHEventSubscriber.class.getName(), Level.WARNING).capture(1);
new CheckRunGHEventSubscriber(jenkinsFacade, scmFacade).onEvent(event);
assertThat(loggerRule.getMessages())
.contains("No proper job found for the rerun request from repository: XiongKezhi/codingstyle and "
+ "branch: PR-1");
}

private GHSubscriberEvent createEventWithRerunRequest(final String jsonFile) throws IOException {
return new GHSubscriberEvent("CheckRunGHEventSubscriberTest", GHEvent.CHECK_RUN,
FileUtils.readFileToString(new File(getClass().getResource(getClass().getSimpleName() + "/"
+ jsonFile).getFile()), StandardCharsets.UTF_8));
}
}
Loading

0 comments on commit 321521d

Please sign in to comment.