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

Implement approval for merge requests #817

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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 in the 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 were introduced in GitLab EE 9.0. They 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