Add Notifier for adding a vote to a GitLab MR (Fixes #168)

This commit is contained in:
Robin Müller 2016-09-04 00:05:20 +02:00
parent adc2c63b27
commit b78c69d1ca
5 changed files with 270 additions and 8 deletions

View File

@ -8,6 +8,7 @@ import com.dabsquared.gitlabjenkins.gitlab.hook.model.NoteHook;
import com.dabsquared.gitlabjenkins.gitlab.hook.model.PushHook;
import com.dabsquared.gitlabjenkins.publisher.GitLabCommitStatusPublisher;
import com.dabsquared.gitlabjenkins.publisher.GitLabMessagePublisher;
import com.dabsquared.gitlabjenkins.publisher.GitLabVotePublisher;
import com.dabsquared.gitlabjenkins.trigger.TriggerOpenMergeRequest;
import com.dabsquared.gitlabjenkins.trigger.branch.ProjectBranchesProvider;
import com.dabsquared.gitlabjenkins.trigger.filter.BranchFilter;
@ -69,7 +70,7 @@ public class GitLabPushTrigger extends Trigger<Job<?, ?>> {
private boolean setBuildDescription = true;
private transient boolean addNoteOnMergeRequest;
private transient boolean addCiMessage;
private boolean addVoteOnMergeRequest = true;
private transient boolean addVoteOnMergeRequest;
private transient boolean allowAllBranches = false;
private transient String branchFilterName;
private BranchFilterType branchFilterType;
@ -145,6 +146,9 @@ public class GitLabPushTrigger extends Trigger<Job<?, ?>> {
if (trigger.addNoteOnMergeRequest) {
project.getPublishersList().add(new GitLabMessagePublisher());
}
if (trigger.addVoteOnMergeRequest) {
project.getPublishersList().add(new GitLabVotePublisher());
}
project.save();
}
}
@ -177,10 +181,6 @@ public class GitLabPushTrigger extends Trigger<Job<?, ?>> {
return setBuildDescription;
}
public boolean getAddVoteOnMergeRequest() {
return addVoteOnMergeRequest;
}
public boolean getAcceptMergeRequestOnSuccess() {
return acceptMergeRequestOnSuccess;
}

View File

@ -0,0 +1,102 @@
package com.dabsquared.gitlabjenkins.publisher;
import com.dabsquared.gitlabjenkins.cause.GitLabWebHookCause;
import com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty;
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabApi;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import org.kohsuke.stapler.DataBoundConstructor;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author Robin Müller
*/
public class GitLabVotePublisher extends Notifier {
private static final Logger LOGGER = Logger.getLogger(GitLabVotePublisher.class.getName());
@DataBoundConstructor
public GitLabVotePublisher() { }
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
addVoteOnMergeRequest(build, listener);
return true;
}
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
@Override
public String getDisplayName() {
return Messages.GitLabVotePublisher_DisplayName();
}
}
private void addVoteOnMergeRequest(Run<?, ?> build, TaskListener listener) {
String projectId = getProjectId(build);
Integer mergeRequestId = getMergeRequestId(build);
if (projectId != null && mergeRequestId != null) {
try {
GitLabApi client = getClient(build);
if (client == null) {
listener.getLogger().println("No GitLab connection configured");
} else {
client.createMergeRequestNote(projectId, mergeRequestId, getResultIcon(build.getResult()));
}
} catch (WebApplicationException | ProcessingException e) {
listener.getLogger().printf("Failed to add vote on Merge Request for project '%s': %s%n", projectId, e.getMessage());
LOGGER.log(Level.SEVERE, String.format("Failed to add vote on Merge Request for project '%s'", projectId), e);
}
}
}
String getProjectId(Run<?, ?> build) {
GitLabWebHookCause cause = build.getCause(GitLabWebHookCause.class);
return cause == null ? null : cause.getData().getTargetProjectId().toString();
}
Integer getMergeRequestId(Run<?, ?> build) {
GitLabWebHookCause cause = build.getCause(GitLabWebHookCause.class);
return cause == null ? null : cause.getData().getMergeRequestId();
}
private String getResultIcon(Result result) {
if (result == Result.SUCCESS) {
return ":+1:";
} else {
return ":-1:";
}
}
private static GitLabApi getClient(Run<?, ?> build) {
GitLabConnectionProperty connectionProperty = build.getParent().getProperty(GitLabConnectionProperty.class);
if (connectionProperty != null) {
return connectionProperty.getClient();
}
return null;
}
}

View File

@ -30,9 +30,6 @@
<f:entry title="Set build description to build cause (eg. Merge request or Git Push )" field="setBuildDescription">
<f:checkbox default="true"/>
</f:entry>
<f:entry title="Vote added to note with build status on merge requests" field="addVoteOnMergeRequest">
<f:checkbox default="true"/>
</f:entry>
<f:entry title="Accept merge request on success" field="acceptMergeRequestOnSuccess">
<f:checkbox default="false"/>
</f:entry>

View File

@ -1,3 +1,4 @@
GitLabCommitStatusPublisher.DisplayName=Publish build status to GitLab commit (GitLab 8.1+ required)
name.required=Build name required.
GitLabMessagePublisher.DisplayName=Add note with build status on GitLab merge requests
GitLabVotePublisher.DisplayName=Add vote for build status on GitLab merge requests

View File

@ -0,0 +1,162 @@
package com.dabsquared.gitlabjenkins.publisher;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.dabsquared.gitlabjenkins.connection.GitLabConnection;
import com.dabsquared.gitlabjenkins.connection.GitLabConnectionConfig;
import com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.StreamBuildListener;
import hudson.plugins.git.util.BuildData;
import hudson.util.Secret;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.mockserver.client.server.MockServerClient;
import org.mockserver.junit.MockServerRule;
import org.mockserver.model.HttpRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
/**
* @author Nikolay Ustinov
*/
public class GitLabVotePublisherTest {
private static final String GIT_LAB_CONNECTION = "GitLab";
private static final String API_TOKEN = "secret";
@ClassRule
public static MockServerRule mockServer = new MockServerRule(new Object());
@ClassRule
public static JenkinsRule jenkins = new JenkinsRule();
private MockServerClient mockServerClient;
private BuildListener listener;
@BeforeClass
public static void setupConnection() throws IOException {
GitLabConnectionConfig connectionConfig = jenkins.get(GitLabConnectionConfig.class);
String apiTokenId = "apiTokenId";
for (CredentialsStore credentialsStore : CredentialsProvider.lookupStores(Jenkins.getInstance())) {
if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) {
List<Domain> domains = credentialsStore.getDomains();
credentialsStore.addCredentials(domains.get(0),
new StringCredentialsImpl(CredentialsScope.SYSTEM, apiTokenId, "GitLab API Token", Secret.fromString(API_TOKEN)));
}
}
connectionConfig.addConnection(new GitLabConnection(GIT_LAB_CONNECTION, "http://localhost:" + mockServer.getPort() + "/gitlab", apiTokenId, false, 10, 10));
}
@Before
public void setup() {
listener = new StreamBuildListener(jenkins.createTaskListener().getLogger(), Charset.defaultCharset());
mockServerClient = new MockServerClient("localhost", mockServer.getPort());
}
@After
public void cleanup() {
mockServerClient.reset();
}
@Test
public void success() throws IOException, InterruptedException {
Integer buildNumber = 1;
String projectId = "3";
Integer mergeRequestId = 1;
AbstractBuild build = mockBuild("/build/123", GIT_LAB_CONNECTION, Result.SUCCESS, buildNumber, projectId);
String buildUrl = Jenkins.getInstance().getRootUrl() + build.getUrl();
String defaultNote = MessageFormat.format(":+1:",
Result.SUCCESS, build.getParent().getDisplayName(), buildNumber, buildUrl);
HttpRequest[] requests = new HttpRequest[] {
prepareSendMessageWithSuccessResponse(projectId, mergeRequestId.toString(), defaultNote)
};
GitLabVotePublisher publisher = spy(new GitLabVotePublisher());
doReturn(projectId).when(publisher).getProjectId(build);
doReturn(mergeRequestId).when(publisher).getMergeRequestId(build);
publisher.perform(build, null, listener);
mockServerClient.verify(requests);
}
@Test
public void failed() throws IOException, InterruptedException {
Integer buildNumber = 1;
String projectId = "3";
Integer mergeRequestId = 1;
AbstractBuild build = mockBuild("/build/123", GIT_LAB_CONNECTION, Result.FAILURE, buildNumber, projectId);
String buildUrl = Jenkins.getInstance().getRootUrl() + build.getUrl();
String defaultNote = MessageFormat.format(":-1:",
Result.FAILURE, build.getParent().getDisplayName(), buildNumber, buildUrl);
HttpRequest[] requests = new HttpRequest[] {
prepareSendMessageWithSuccessResponse(projectId, mergeRequestId.toString(), defaultNote)
};
GitLabVotePublisher publisher = spy(new GitLabVotePublisher());
doReturn(projectId).when(publisher).getProjectId(build);
doReturn(mergeRequestId).when(publisher).getMergeRequestId(build);
publisher.perform(build, null, listener);
mockServerClient.verify(requests);
}
private HttpRequest prepareSendMessageWithSuccessResponse(String projectId, String mergeRequestId, String body) throws UnsupportedEncodingException {
HttpRequest updateCommitStatus = prepareSendMessageStatus(projectId, mergeRequestId, body);
mockServerClient.when(updateCommitStatus).respond(response().withStatusCode(200));
return updateCommitStatus;
}
private HttpRequest prepareSendMessageStatus(String projectId, String mergeRequestId, String body) throws UnsupportedEncodingException {
return request()
.withPath("/gitlab/api/v3/projects/" + URLEncoder.encode(projectId, "UTF-8") + "/merge_requests/" + URLEncoder.encode(mergeRequestId, "UTF-8") + "/notes")
.withMethod("POST")
.withHeader("PRIVATE-TOKEN", "secret")
.withQueryStringParameter("body", body);
}
private AbstractBuild mockBuild(String buildUrl, String gitLabConnection, Result result, Integer buildNumber, String... remoteUrls) {
AbstractBuild build = mock(AbstractBuild.class);
BuildData buildData = mock(BuildData.class);
when(buildData.getRemoteUrls()).thenReturn(new HashSet<>(Arrays.asList(remoteUrls)));
when(build.getAction(BuildData.class)).thenReturn(buildData);
when(build.getResult()).thenReturn(result);
when(build.getUrl()).thenReturn(buildUrl);
when(build.getResult()).thenReturn(result);
when(build.getUrl()).thenReturn(buildUrl);
when(build.getNumber()).thenReturn(buildNumber);
AbstractProject<?, ?> project = mock(AbstractProject.class);
when(project.getProperty(GitLabConnectionProperty.class)).thenReturn(new GitLabConnectionProperty(gitLabConnection));
when(build.getProject()).thenReturn(project);
return build;
}
}