Skip to content

Commit

Permalink
Implement approval for merge requests
Browse files Browse the repository at this point in the history
  • Loading branch information
padyx committed Dec 9, 2018
1 parent 81f9ffd commit 2e32ab3
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public interface GitLabClient {

void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId);

void approveMergeRequest(MergeRequest mr);

void unapproveMergeRequest(MergeRequest mr);

List<MergeRequest> getMergeRequests(String projectId, State state, int page, int perPage);

List<Branch> getBranches(String projectId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,32 @@ Void execute(GitLabClient client) {
);
}

@Override
public void approveMergeRequest(final MergeRequest mr) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabClient client) {
client.approveMergeRequest(mr);
return null;
}
}
);
}

@Override
public void unapproveMergeRequest(final MergeRequest mr) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabClient client) {
client.unapproveMergeRequest(mr);
return null;
}
}
);
}

@Override
public List<MergeRequest> getMergeRequests(final String projectId, final State state, final int page, final int perPage) {
return execute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ interface GitLabApiProxy {

void deleteMergeRequestEmoji(Integer projectId, Integer mergeRequestId, Integer awardId);

void approveMergeRequest(Integer projectId, Integer mergeRequestId);

void unapproveMergeRequest(Integer projectId, Integer mergeRequestId);

List<MergeRequest> getMergeRequests(String projectId, State state, int page, int perPage);

List<Branch> getBranches(String projectId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ public void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId) {
api.deleteMergeRequestEmoji(mr.getProjectId(), mergeRequestIdProvider.apply(mr), awardId);
}

@Override
public void approveMergeRequest(MergeRequest mr) {
api.approveMergeRequest(mr.getProjectId(), mergeRequestIdProvider.apply(mr));
}

@Override
public void unapproveMergeRequest(MergeRequest mr) {
api.unapproveMergeRequest(mr.getProjectId(), mergeRequestIdProvider.apply(mr));
}

@Override
public List<MergeRequest> getMergeRequests(String projectId, State state, int page, int perPage) {
return api.getMergeRequests(projectId, state, page, perPage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ MergeRequest createMergeRequest(
@FormParam("target_branch") String targetBranch,
@FormParam("title") String title);

/**
* Unsupported in API v3
*/
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path("/projects/{projectId}/merge_requests/{mergeRequestIid}/approve")
@Override
void approveMergeRequest(@PathParam("projectId") Integer projectId, @PathParam("mergeRequestIid") Integer mergeRequestId);

/**
* Unsupported in API v3
*/
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path("/projects/{projectId}/merge_requests/{mergeRequestIid}/unapprove")
@Override
void unapproveMergeRequest(Integer projectId, Integer mergeRequestId);

@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/projects/{projectName}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ MergeRequest createMergeRequest(
@FormParam("target_branch") String targetBranch,
@FormParam("title") String title);

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path("/projects/{projectId}/merge_requests/{mergeRequestIid}/approve")
@Override
void approveMergeRequest(@PathParam("projectId") Integer projectId, @PathParam("mergeRequestIid") Integer mergeRequestId);

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path("/projects/{projectId}/merge_requests/{mergeRequestIid}/unapprove")
@Override
void unapproveMergeRequest(@PathParam("projectId") Integer projectId, @PathParam("mergeRequestIid") Integer mergeRequestId);

@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/projects/{projectName}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.dabsquared.gitlabjenkins.publisher;

import com.dabsquared.gitlabjenkins.gitlab.api.GitLabClient;
import com.dabsquared.gitlabjenkins.gitlab.api.model.MergeRequest;
import hudson.Extension;
import hudson.model.AbstractProject;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Publisher;
import org.kohsuke.stapler.DataBoundConstructor;

import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class GitLabApproveMergeRequestPublisher extends MergeRequestNotifier {
private static final Logger LOGGER = Logger.getLogger(GitLabApproveMergeRequestPublisher.class.getName());

private final boolean approveUnstableBuilds;

@DataBoundConstructor
public GitLabApproveMergeRequestPublisher(boolean approveUnstableBuilds) {
this.approveUnstableBuilds = approveUnstableBuilds;
}

@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {

@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}

@Override
public String getHelpFile() {
return "/plugin/gitlab-plugin/help/help-approve-gitlab-mergerequest.html";
}

@Override
public String getDisplayName() {
return Messages.GitLabApproveMergeRequestPublisher_DisplayName();
}
}

public boolean isApproveUnstableBuilds() {
return approveUnstableBuilds;
}

@Override
protected void perform(Run<?, ?> build, TaskListener listener, GitLabClient client, MergeRequest mergeRequest) {
try {
Result buildResult = build.getResult();
if (build.getResult() == Result.SUCCESS || (buildResult == Result.UNSTABLE && isApproveUnstableBuilds())) {
client.approveMergeRequest(mergeRequest);
} else {
client.unapproveMergeRequest(mergeRequest);
}
} catch (NotFoundException e) {
String message = String.format(
"Failed to approve/unapprove merge request '%s' for project '%s'.\n"
+ "Got unexpected 404. Does your GitLab edition or GitLab.com tier really support approvals, and are you are an eligible approver for this merge request?", mergeRequest.getIid(), mergeRequest.getProjectId());
listener.getLogger().printf(message);
LOGGER.log(Level.WARNING, message, e);
} catch (NotAuthorizedException e) {
String message = String.format(
"Failed to approve/unapprove merge request '%s' for project '%s'.\n"
+ "Got unexpected 401, are you using the wrong credentials?", mergeRequest.getIid(), mergeRequest.getProjectId());
listener.getLogger().printf(message);
LOGGER.log(Level.WARNING, message, e);
} catch (WebApplicationException | ProcessingException e) {
listener.getLogger().printf("Failed to approve/unapprove merge request for project '%s': %s%n", mergeRequest.getProjectId(), e.getMessage());
LOGGER.log(Level.SEVERE, String.format("Failed to approve/unapprove merge request for project '%s'", mergeRequest.getProjectId()), e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:advanced>
<f:entry title="${%Approve unstable builds}" field="approveUnstableBuilds">
<f:checkbox default="false"/>
</f:entry>
</f:advanced>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ 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
GitLabAcceptMergeRequestPublisher.DisplayName=Accept GitLab merge request on success
GitLabApproveMergeRequestPublisher.DisplayName=Approve / Revoke approval on GitLab merge request (EE-only)
8 changes: 8 additions & 0 deletions src/main/webapp/help/help-approve-gitlab-mergerequest.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div>
Approve the GitLab merge request if builds succeeds, or revoke the approval if it fails.<br/>
The approval will be visible merge request UI. You must have at least one GitLab connection/server configured in the Jenkins global configuration.
<p>
<b><span style="background-color:#ff5c33;">Feature availability warning:</span></b>
Merge request approvals are not available in every <a href="https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html" target="_blank">GitLab Edition or GitLab.com tier</a>.
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.dabsquared.gitlabjenkins.publisher;

import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.StreamBuildListener;
import org.junit.*;
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.nio.charset.Charset;

import static com.dabsquared.gitlabjenkins.publisher.TestUtility.*;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

public class GitLabApproveMergeRequestPublisherTest {

@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 setupClass() throws IOException {
setupGitLabConnections(jenkins, mockServer);
}

@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 matrixAggregatable() throws InterruptedException, IOException {
verifyMatrixAggregatable(GitLabApproveMergeRequestPublisher.class, listener);
}

@Test
public void success_approve_unstable_v4() throws IOException, InterruptedException {
performApprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.SUCCESS), "v4", MERGE_REQUEST_IID, true);
}

@Test
public void success_v4() throws IOException, InterruptedException {
performApprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.SUCCESS), "v4", MERGE_REQUEST_IID, false);
}

@Test
public void unstable_approve_unstable_v4() throws IOException, InterruptedException {
performApprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.UNSTABLE), "v4", MERGE_REQUEST_IID, true);
}

@Test
public void unstable_dontapprove_v4() throws IOException, InterruptedException {
performUnapprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.UNSTABLE), "v4", MERGE_REQUEST_IID, false);
}

@Test
public void failed_approve_unstable_v4() throws IOException, InterruptedException {
performUnapprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.FAILURE), "v4", MERGE_REQUEST_IID, true);
}

@Test
public void failed_v4() throws IOException, InterruptedException {
performUnapprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.FAILURE), "v4", MERGE_REQUEST_IID, false);
}

private void performApprovalAndVerify(AbstractBuild build, String apiLevel, int mergeRequestId, boolean approveUnstable) throws InterruptedException, IOException {
GitLabApproveMergeRequestPublisher publisher = preparePublisher(new GitLabApproveMergeRequestPublisher(approveUnstable), build);
publisher.perform(build, null, listener);

mockServerClient.verify(prepareSendApprovalWithSuccessResponse(build, apiLevel, mergeRequestId));
}

private void performUnapprovalAndVerify(AbstractBuild build, String apiLevel, int mergeRequestId, boolean approveUnstable) throws InterruptedException, IOException {
GitLabApproveMergeRequestPublisher publisher = preparePublisher(new GitLabApproveMergeRequestPublisher(approveUnstable), build);
publisher.perform(build, null, listener);

mockServerClient.verify(prepareSendUnapprovalWithSuccessResponse(build, apiLevel, mergeRequestId));
}

private HttpRequest prepareSendApprovalWithSuccessResponse(AbstractBuild build, String apiLevel, int mergeRequestId) throws UnsupportedEncodingException {
HttpRequest approvalRequest = prepareSendApproval(apiLevel, mergeRequestId);
mockServerClient.when(approvalRequest).respond(response().withStatusCode(200));
return approvalRequest;
}

private HttpRequest prepareSendUnapprovalWithSuccessResponse(AbstractBuild build, String apiLevel, int mergeRequestId) throws UnsupportedEncodingException {
HttpRequest unapprovalRequest = prepareSendUnapproval(apiLevel, mergeRequestId);
mockServerClient.when(unapprovalRequest).respond(response().withStatusCode(200));
return unapprovalRequest;
}

private HttpRequest prepareSendApproval(final String apiLevel, int mergeRequestId) throws UnsupportedEncodingException {
return request()
.withPath("/gitlab/api/" + apiLevel + "/projects/" + PROJECT_ID + "/merge_requests/" + mergeRequestId + "/approve")
.withMethod("POST")
.withHeader("PRIVATE-TOKEN", "secret");
}

private HttpRequest prepareSendUnapproval(final String apiLevel, int mergeRequestId) throws UnsupportedEncodingException {
return request()
.withPath("/gitlab/api/" + apiLevel + "/projects/" + PROJECT_ID + "/merge_requests/" + mergeRequestId + "/unapprove")
.withMethod("POST")
.withHeader("PRIVATE-TOKEN", "secret");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ public List<Awardable> getMergeRequestEmoji(MergeRequest mr) {
@Override
public void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId) {

}

@Override
public void approveMergeRequest(MergeRequest mr) {

}

@Override
public void unapproveMergeRequest(MergeRequest mr) {

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ public void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId) {

}

@Override
public void approveMergeRequest(MergeRequest mr) {

}

@Override
public void unapproveMergeRequest(MergeRequest mr) {

}

@Override
public List<MergeRequest> getMergeRequests(String projectId, State state, int page, int perPage) {
return null;
Expand Down

0 comments on commit 2e32ab3

Please sign in to comment.