-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #25 from jenkinsci/rerun-action
Add subscriber for check run event in order to handle rerun request
- Loading branch information
Showing
6 changed files
with
1,239 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
src/main/java/io/jenkins/plugins/checks/github/CheckRunGHEventSubscriber.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
213 changes: 213 additions & 0 deletions
213
src/test/java/io/jenkins/plugins/checks/github/CheckRunGHEventSubscriberTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
Oops, something went wrong.