diff --git a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/GitLabClient.java b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/GitLabClient.java index 1ac539c9b..2b18c921f 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/GitLabClient.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/GitLabClient.java @@ -36,6 +36,10 @@ public interface GitLabClient { void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId); + void approveMergeRequest(MergeRequest mr); + + void unapproveMergeRequest(MergeRequest mr); + List getMergeRequests(String projectId, State state, int page, int perPage); List getBranches(String projectId); diff --git a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/AutodetectingGitLabClient.java b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/AutodetectingGitLabClient.java index 10525d770..d426c7b34 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/AutodetectingGitLabClient.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/AutodetectingGitLabClient.java @@ -204,6 +204,32 @@ Void execute(GitLabClient client) { ); } + @Override + public void approveMergeRequest(final MergeRequest mr) { + execute( + new GitLabOperation() { + @Override + Void execute(GitLabClient client) { + client.approveMergeRequest(mr); + return null; + } + } + ); + } + + @Override + public void unapproveMergeRequest(final MergeRequest mr) { + execute( + new GitLabOperation() { + @Override + Void execute(GitLabClient client) { + client.unapproveMergeRequest(mr); + return null; + } + } + ); + } + @Override public List getMergeRequests(final String projectId, final State state, final int page, final int perPage) { return execute( diff --git a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/GitLabApiProxy.java b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/GitLabApiProxy.java index 70d4a75cb..4d88a529f 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/GitLabApiProxy.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/GitLabApiProxy.java @@ -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 getMergeRequests(String projectId, State state, int page, int perPage); List getBranches(String projectId); diff --git a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/ResteasyGitLabClient.java b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/ResteasyGitLabClient.java index 526b4fecf..377b65105 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/ResteasyGitLabClient.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/ResteasyGitLabClient.java @@ -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 getMergeRequests(String projectId, State state, int page, int perPage) { return api.getMergeRequests(projectId, state, page, perPage); diff --git a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V3GitLabApiProxy.java b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V3GitLabApiProxy.java index 5fc1ec046..cda77ff16 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V3GitLabApiProxy.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V3GitLabApiProxy.java @@ -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}") diff --git a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V4GitLabApiProxy.java b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V4GitLabApiProxy.java index 1987096f6..f930d7a00 100644 --- a/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V4GitLabApiProxy.java +++ b/src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V4GitLabApiProxy.java @@ -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}") diff --git a/src/main/java/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisher.java b/src/main/java/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisher.java new file mode 100644 index 000000000..7ce96a241 --- /dev/null +++ b/src/main/java/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisher.java @@ -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 { + + @Override + public boolean isApplicable(Class 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); + } + } + +} diff --git a/src/main/resources/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisher/config.jelly b/src/main/resources/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisher/config.jelly new file mode 100644 index 000000000..2638b0fbb --- /dev/null +++ b/src/main/resources/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisher/config.jelly @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/com/dabsquared/gitlabjenkins/publisher/Messages.properties b/src/main/resources/com/dabsquared/gitlabjenkins/publisher/Messages.properties index 1211b70bc..244135686 100644 --- a/src/main/resources/com/dabsquared/gitlabjenkins/publisher/Messages.properties +++ b/src/main/resources/com/dabsquared/gitlabjenkins/publisher/Messages.properties @@ -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) diff --git a/src/main/webapp/help/help-approve-gitlab-mergerequest.html b/src/main/webapp/help/help-approve-gitlab-mergerequest.html new file mode 100644 index 000000000..cdd6afce7 --- /dev/null +++ b/src/main/webapp/help/help-approve-gitlab-mergerequest.html @@ -0,0 +1,8 @@ +
+ Approve the GitLab merge request if builds succeeds, or revoke the approval if it fails.
+ The approval will be visible merge request UI. You must have at least one GitLab connection/server configured in the Jenkins global configuration. +

+ Feature availability warning: + Merge request approvals are not available in every GitLab Edition or GitLab.com tier. +

+
diff --git a/src/test/java/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisherTest.java b/src/test/java/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisherTest.java new file mode 100644 index 000000000..849e1245a --- /dev/null +++ b/src/test/java/com/dabsquared/gitlabjenkins/publisher/GitLabApproveMergeRequestPublisherTest.java @@ -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"); + } + +} diff --git a/src/test/java/com/dabsquared/gitlabjenkins/service/GitLabClientStub.java b/src/test/java/com/dabsquared/gitlabjenkins/service/GitLabClientStub.java index 82c80c769..57f4b8ada 100644 --- a/src/test/java/com/dabsquared/gitlabjenkins/service/GitLabClientStub.java +++ b/src/test/java/com/dabsquared/gitlabjenkins/service/GitLabClientStub.java @@ -142,6 +142,16 @@ public List getMergeRequestEmoji(MergeRequest mr) { @Override public void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId) { + } + + @Override + public void approveMergeRequest(MergeRequest mr) { + + } + + @Override + public void unapproveMergeRequest(MergeRequest mr) { + } @Override diff --git a/src/test/java/com/dabsquared/gitlabjenkins/util/GitLabClientStub.java b/src/test/java/com/dabsquared/gitlabjenkins/util/GitLabClientStub.java index e29c1e286..62c09660c 100644 --- a/src/test/java/com/dabsquared/gitlabjenkins/util/GitLabClientStub.java +++ b/src/test/java/com/dabsquared/gitlabjenkins/util/GitLabClientStub.java @@ -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 getMergeRequests(String projectId, State state, int page, int perPage) { return null;