Skip to content

Commit 2e32ab3

Browse files
committed
Implement approval for merge requests
1 parent 81f9ffd commit 2e32ab3

File tree

13 files changed

+319
-0
lines changed

13 files changed

+319
-0
lines changed

src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/GitLabClient.java

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public interface GitLabClient {
3636

3737
void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId);
3838

39+
void approveMergeRequest(MergeRequest mr);
40+
41+
void unapproveMergeRequest(MergeRequest mr);
42+
3943
List<MergeRequest> getMergeRequests(String projectId, State state, int page, int perPage);
4044

4145
List<Branch> getBranches(String projectId);

src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/AutodetectingGitLabClient.java

+26
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,32 @@ Void execute(GitLabClient client) {
204204
);
205205
}
206206

207+
@Override
208+
public void approveMergeRequest(final MergeRequest mr) {
209+
execute(
210+
new GitLabOperation<Void>() {
211+
@Override
212+
Void execute(GitLabClient client) {
213+
client.approveMergeRequest(mr);
214+
return null;
215+
}
216+
}
217+
);
218+
}
219+
220+
@Override
221+
public void unapproveMergeRequest(final MergeRequest mr) {
222+
execute(
223+
new GitLabOperation<Void>() {
224+
@Override
225+
Void execute(GitLabClient client) {
226+
client.unapproveMergeRequest(mr);
227+
return null;
228+
}
229+
}
230+
);
231+
}
232+
207233
@Override
208234
public List<MergeRequest> getMergeRequests(final String projectId, final State state, final int page, final int perPage) {
209235
return execute(

src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/GitLabApiProxy.java

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ interface GitLabApiProxy {
3636

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

39+
void approveMergeRequest(Integer projectId, Integer mergeRequestId);
40+
41+
void unapproveMergeRequest(Integer projectId, Integer mergeRequestId);
42+
3943
List<MergeRequest> getMergeRequests(String projectId, State state, int page, int perPage);
4044

4145
List<Branch> getBranches(String projectId);

src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/ResteasyGitLabClient.java

+10
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ public void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId) {
9595
api.deleteMergeRequestEmoji(mr.getProjectId(), mergeRequestIdProvider.apply(mr), awardId);
9696
}
9797

98+
@Override
99+
public void approveMergeRequest(MergeRequest mr) {
100+
api.approveMergeRequest(mr.getProjectId(), mergeRequestIdProvider.apply(mr));
101+
}
102+
103+
@Override
104+
public void unapproveMergeRequest(MergeRequest mr) {
105+
api.unapproveMergeRequest(mr.getProjectId(), mergeRequestIdProvider.apply(mr));
106+
}
107+
98108
@Override
99109
public List<MergeRequest> getMergeRequests(String projectId, State state, int page, int perPage) {
100110
return api.getMergeRequests(projectId, state, page, perPage);

src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V3GitLabApiProxy.java

+20
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,26 @@ MergeRequest createMergeRequest(
4646
@FormParam("target_branch") String targetBranch,
4747
@FormParam("title") String title);
4848

49+
/**
50+
* Unsupported in API v3
51+
*/
52+
@POST
53+
@Produces(MediaType.APPLICATION_JSON)
54+
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
55+
@Path("/projects/{projectId}/merge_requests/{mergeRequestIid}/approve")
56+
@Override
57+
void approveMergeRequest(@PathParam("projectId") Integer projectId, @PathParam("mergeRequestIid") Integer mergeRequestId);
58+
59+
/**
60+
* Unsupported in API v3
61+
*/
62+
@POST
63+
@Produces(MediaType.APPLICATION_JSON)
64+
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
65+
@Path("/projects/{projectId}/merge_requests/{mergeRequestIid}/unapprove")
66+
@Override
67+
void unapproveMergeRequest(Integer projectId, Integer mergeRequestId);
68+
4969
@GET
5070
@Produces(MediaType.APPLICATION_JSON)
5171
@Path("/projects/{projectName}")

src/main/java/com/dabsquared/gitlabjenkins/gitlab/api/impl/V4GitLabApiProxy.java

+14
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ MergeRequest createMergeRequest(
4646
@FormParam("target_branch") String targetBranch,
4747
@FormParam("title") String title);
4848

49+
@POST
50+
@Produces(MediaType.APPLICATION_JSON)
51+
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
52+
@Path("/projects/{projectId}/merge_requests/{mergeRequestIid}/approve")
53+
@Override
54+
void approveMergeRequest(@PathParam("projectId") Integer projectId, @PathParam("mergeRequestIid") Integer mergeRequestId);
55+
56+
@POST
57+
@Produces(MediaType.APPLICATION_JSON)
58+
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
59+
@Path("/projects/{projectId}/merge_requests/{mergeRequestIid}/unapprove")
60+
@Override
61+
void unapproveMergeRequest(@PathParam("projectId") Integer projectId, @PathParam("mergeRequestIid") Integer mergeRequestId);
62+
4963
@GET
5064
@Produces(MediaType.APPLICATION_JSON)
5165
@Path("/projects/{projectName}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.dabsquared.gitlabjenkins.publisher;
2+
3+
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabClient;
4+
import com.dabsquared.gitlabjenkins.gitlab.api.model.MergeRequest;
5+
import hudson.Extension;
6+
import hudson.model.AbstractProject;
7+
import hudson.model.Result;
8+
import hudson.model.Run;
9+
import hudson.model.TaskListener;
10+
import hudson.tasks.BuildStepDescriptor;
11+
import hudson.tasks.Publisher;
12+
import org.kohsuke.stapler.DataBoundConstructor;
13+
14+
import javax.ws.rs.NotAuthorizedException;
15+
import javax.ws.rs.NotFoundException;
16+
import javax.ws.rs.ProcessingException;
17+
import javax.ws.rs.WebApplicationException;
18+
import java.util.logging.Level;
19+
import java.util.logging.Logger;
20+
21+
public class GitLabApproveMergeRequestPublisher extends MergeRequestNotifier {
22+
private static final Logger LOGGER = Logger.getLogger(GitLabApproveMergeRequestPublisher.class.getName());
23+
24+
private final boolean approveUnstableBuilds;
25+
26+
@DataBoundConstructor
27+
public GitLabApproveMergeRequestPublisher(boolean approveUnstableBuilds) {
28+
this.approveUnstableBuilds = approveUnstableBuilds;
29+
}
30+
31+
@Extension
32+
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
33+
34+
@Override
35+
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
36+
return true;
37+
}
38+
39+
@Override
40+
public String getHelpFile() {
41+
return "/plugin/gitlab-plugin/help/help-approve-gitlab-mergerequest.html";
42+
}
43+
44+
@Override
45+
public String getDisplayName() {
46+
return Messages.GitLabApproveMergeRequestPublisher_DisplayName();
47+
}
48+
}
49+
50+
public boolean isApproveUnstableBuilds() {
51+
return approveUnstableBuilds;
52+
}
53+
54+
@Override
55+
protected void perform(Run<?, ?> build, TaskListener listener, GitLabClient client, MergeRequest mergeRequest) {
56+
try {
57+
Result buildResult = build.getResult();
58+
if (build.getResult() == Result.SUCCESS || (buildResult == Result.UNSTABLE && isApproveUnstableBuilds())) {
59+
client.approveMergeRequest(mergeRequest);
60+
} else {
61+
client.unapproveMergeRequest(mergeRequest);
62+
}
63+
} catch (NotFoundException e) {
64+
String message = String.format(
65+
"Failed to approve/unapprove merge request '%s' for project '%s'.\n"
66+
+ "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());
67+
listener.getLogger().printf(message);
68+
LOGGER.log(Level.WARNING, message, e);
69+
} catch (NotAuthorizedException e) {
70+
String message = String.format(
71+
"Failed to approve/unapprove merge request '%s' for project '%s'.\n"
72+
+ "Got unexpected 401, are you using the wrong credentials?", mergeRequest.getIid(), mergeRequest.getProjectId());
73+
listener.getLogger().printf(message);
74+
LOGGER.log(Level.WARNING, message, e);
75+
} catch (WebApplicationException | ProcessingException e) {
76+
listener.getLogger().printf("Failed to approve/unapprove merge request for project '%s': %s%n", mergeRequest.getProjectId(), e.getMessage());
77+
LOGGER.log(Level.SEVERE, String.format("Failed to approve/unapprove merge request for project '%s'", mergeRequest.getProjectId()), e);
78+
}
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
3+
<f:advanced>
4+
<f:entry title="${%Approve unstable builds}" field="approveUnstableBuilds">
5+
<f:checkbox default="false"/>
6+
</f:entry>
7+
</f:advanced>
8+
</j:jelly>

src/main/resources/com/dabsquared/gitlabjenkins/publisher/Messages.properties

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ name.required=Build name required.
33
GitLabMessagePublisher.DisplayName=Add note with build status on GitLab merge requests
44
GitLabVotePublisher.DisplayName=Add vote for build status on GitLab merge requests
55
GitLabAcceptMergeRequestPublisher.DisplayName=Accept GitLab merge request on success
6+
GitLabApproveMergeRequestPublisher.DisplayName=Approve / Revoke approval on GitLab merge request (EE-only)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div>
2+
Approve the GitLab merge request if builds succeeds, or revoke the approval if it fails.<br/>
3+
The approval will be visible merge request UI. You must have at least one GitLab connection/server configured in the Jenkins global configuration.
4+
<p>
5+
<b><span style="background-color:#ff5c33;">Feature availability warning:</span></b>
6+
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>.
7+
</p>
8+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package com.dabsquared.gitlabjenkins.publisher;
2+
3+
import hudson.model.AbstractBuild;
4+
import hudson.model.BuildListener;
5+
import hudson.model.Result;
6+
import hudson.model.StreamBuildListener;
7+
import org.junit.*;
8+
import org.jvnet.hudson.test.JenkinsRule;
9+
import org.mockserver.client.server.MockServerClient;
10+
import org.mockserver.junit.MockServerRule;
11+
import org.mockserver.model.HttpRequest;
12+
13+
import java.io.IOException;
14+
import java.io.UnsupportedEncodingException;
15+
import java.nio.charset.Charset;
16+
17+
import static com.dabsquared.gitlabjenkins.publisher.TestUtility.*;
18+
import static org.mockserver.model.HttpRequest.request;
19+
import static org.mockserver.model.HttpResponse.response;
20+
21+
public class GitLabApproveMergeRequestPublisherTest {
22+
23+
@ClassRule
24+
public static MockServerRule mockServer = new MockServerRule(new Object());
25+
26+
@ClassRule
27+
public static JenkinsRule jenkins = new JenkinsRule();
28+
29+
private MockServerClient mockServerClient;
30+
private BuildListener listener;
31+
32+
@BeforeClass
33+
public static void setupClass() throws IOException {
34+
setupGitLabConnections(jenkins, mockServer);
35+
}
36+
37+
@Before
38+
public void setup() {
39+
listener = new StreamBuildListener(jenkins.createTaskListener().getLogger(), Charset.defaultCharset());
40+
mockServerClient = new MockServerClient("localhost", mockServer.getPort());
41+
}
42+
43+
@After
44+
public void cleanup() {
45+
mockServerClient.reset();
46+
}
47+
48+
@Test
49+
public void matrixAggregatable() throws InterruptedException, IOException {
50+
verifyMatrixAggregatable(GitLabApproveMergeRequestPublisher.class, listener);
51+
}
52+
53+
@Test
54+
public void success_approve_unstable_v4() throws IOException, InterruptedException {
55+
performApprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.SUCCESS), "v4", MERGE_REQUEST_IID, true);
56+
}
57+
58+
@Test
59+
public void success_v4() throws IOException, InterruptedException {
60+
performApprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.SUCCESS), "v4", MERGE_REQUEST_IID, false);
61+
}
62+
63+
@Test
64+
public void unstable_approve_unstable_v4() throws IOException, InterruptedException {
65+
performApprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.UNSTABLE), "v4", MERGE_REQUEST_IID, true);
66+
}
67+
68+
@Test
69+
public void unstable_dontapprove_v4() throws IOException, InterruptedException {
70+
performUnapprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.UNSTABLE), "v4", MERGE_REQUEST_IID, false);
71+
}
72+
73+
@Test
74+
public void failed_approve_unstable_v4() throws IOException, InterruptedException {
75+
performUnapprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.FAILURE), "v4", MERGE_REQUEST_IID, true);
76+
}
77+
78+
@Test
79+
public void failed_v4() throws IOException, InterruptedException {
80+
performUnapprovalAndVerify(mockSimpleBuild(GITLAB_CONNECTION_V4, Result.FAILURE), "v4", MERGE_REQUEST_IID, false);
81+
}
82+
83+
private void performApprovalAndVerify(AbstractBuild build, String apiLevel, int mergeRequestId, boolean approveUnstable) throws InterruptedException, IOException {
84+
GitLabApproveMergeRequestPublisher publisher = preparePublisher(new GitLabApproveMergeRequestPublisher(approveUnstable), build);
85+
publisher.perform(build, null, listener);
86+
87+
mockServerClient.verify(prepareSendApprovalWithSuccessResponse(build, apiLevel, mergeRequestId));
88+
}
89+
90+
private void performUnapprovalAndVerify(AbstractBuild build, String apiLevel, int mergeRequestId, boolean approveUnstable) throws InterruptedException, IOException {
91+
GitLabApproveMergeRequestPublisher publisher = preparePublisher(new GitLabApproveMergeRequestPublisher(approveUnstable), build);
92+
publisher.perform(build, null, listener);
93+
94+
mockServerClient.verify(prepareSendUnapprovalWithSuccessResponse(build, apiLevel, mergeRequestId));
95+
}
96+
97+
private HttpRequest prepareSendApprovalWithSuccessResponse(AbstractBuild build, String apiLevel, int mergeRequestId) throws UnsupportedEncodingException {
98+
HttpRequest approvalRequest = prepareSendApproval(apiLevel, mergeRequestId);
99+
mockServerClient.when(approvalRequest).respond(response().withStatusCode(200));
100+
return approvalRequest;
101+
}
102+
103+
private HttpRequest prepareSendUnapprovalWithSuccessResponse(AbstractBuild build, String apiLevel, int mergeRequestId) throws UnsupportedEncodingException {
104+
HttpRequest unapprovalRequest = prepareSendUnapproval(apiLevel, mergeRequestId);
105+
mockServerClient.when(unapprovalRequest).respond(response().withStatusCode(200));
106+
return unapprovalRequest;
107+
}
108+
109+
private HttpRequest prepareSendApproval(final String apiLevel, int mergeRequestId) throws UnsupportedEncodingException {
110+
return request()
111+
.withPath("/gitlab/api/" + apiLevel + "/projects/" + PROJECT_ID + "/merge_requests/" + mergeRequestId + "/approve")
112+
.withMethod("POST")
113+
.withHeader("PRIVATE-TOKEN", "secret");
114+
}
115+
116+
private HttpRequest prepareSendUnapproval(final String apiLevel, int mergeRequestId) throws UnsupportedEncodingException {
117+
return request()
118+
.withPath("/gitlab/api/" + apiLevel + "/projects/" + PROJECT_ID + "/merge_requests/" + mergeRequestId + "/unapprove")
119+
.withMethod("POST")
120+
.withHeader("PRIVATE-TOKEN", "secret");
121+
}
122+
123+
}

src/test/java/com/dabsquared/gitlabjenkins/service/GitLabClientStub.java

+10
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ public List<Awardable> getMergeRequestEmoji(MergeRequest mr) {
142142
@Override
143143
public void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId) {
144144

145+
}
146+
147+
@Override
148+
public void approveMergeRequest(MergeRequest mr) {
149+
150+
}
151+
152+
@Override
153+
public void unapproveMergeRequest(MergeRequest mr) {
154+
145155
}
146156

147157
@Override

src/test/java/com/dabsquared/gitlabjenkins/util/GitLabClientStub.java

+10
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ public void deleteMergeRequestEmoji(MergeRequest mr, Integer awardId) {
8888

8989
}
9090

91+
@Override
92+
public void approveMergeRequest(MergeRequest mr) {
93+
94+
}
95+
96+
@Override
97+
public void unapproveMergeRequest(MergeRequest mr) {
98+
99+
}
100+
91101
@Override
92102
public List<MergeRequest> getMergeRequests(String projectId, State state, int page, int perPage) {
93103
return null;

0 commit comments

Comments
 (0)