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 all 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 @@ -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()) {
XiongKezhi marked this conversation as resolved.
Show resolved Hide resolved
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