From 65b230e5f85b46f7b110d6ecf7e2de3f1255d90e Mon Sep 17 00:00:00 2001 From: Tariq Ettaji Date: Tue, 7 Nov 2017 13:52:41 +0100 Subject: [PATCH] Added options to mark a pull request as Approved or Needs Work --- README.md | 4 +- pom.xml | 6 + .../StashBuildTrigger.java | 16 ++- .../stashpullrequestbuilder/StashBuilds.java | 10 ++ .../StashMarkStatus.java | 21 ++++ .../StashRepository.java | 4 + .../stash/StashApiClient.java | 104 ++++++++++++++++++ .../StashBuildTrigger/config.jelly | 6 + .../StashMarkStatusTest.java | 58 ++++++++++ 9 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashMarkStatus.java create mode 100644 src/test/java/stashpullrequestbuilder/stashpullrequestbuilder/StashMarkStatusTest.java diff --git a/README.md b/README.md index 5c699667..f83d4cc6 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ Select *Stash Pull Request Builder* then configure: - Only build when asked (with test phrase): - CI Build Phrases: default: "test this please" - Target branches: a comma separated list of branches (e.g. brancha,branchb) +- Approve PR on build success: marks a pull request as Approved with the specified user. Make sure the user has sufficient rights to update the status of pull requests and the username is "slugified". +- Mark PR with Needs Work on build failure: As above, but with failed builds. The same user requirements apply. ## Building the merge of Source Branch into Target Branch @@ -67,7 +69,7 @@ You may want Jenkins to build the merged PR (that is the merge of `sourceBranch` If you are building the merged PR you probably want Jenkins to do a new build when the target branch changes. There is an advanced option in the build trigger, "Rebuild if destination branch changes?" which enables this. -You probably also only want to build if the PR was mergeable and always without conflicts. There are advanced options in the build trigger for both of these. +You probably also only want to build if the PR was mergeable and always without conflicts. There are advanced options in the build trigger for both of these. **NOTE: *Always enable `Build only if Stash reports no conflicts` if using the merge RefSpec!*** This will make sure the lazy merge on stash has happened before the build is triggered. diff --git a/pom.xml b/pom.xml index b93cfb32..aaf42116 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,12 @@ credentials 2.1.5 + + org.mockito + mockito-core + 2.7.22 + test + diff --git a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger.java b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger.java index a91bf676..b2071aad 100644 --- a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger.java +++ b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger.java @@ -69,6 +69,8 @@ public class StashBuildTrigger extends Trigger> { private final boolean onlyBuildOnComment; private final boolean deletePreviousBuildFinishComments; private final boolean cancelOutdatedJobsEnabled; + private final boolean approveOnBuildSuccessful; + private final boolean needsWorkOnBuildFailure; transient private StashPullRequestsBuilder stashPullRequestsBuilder; @@ -93,7 +95,9 @@ public StashBuildTrigger( String ciBuildPhrases, boolean deletePreviousBuildFinishComments, String targetBranchesToBuild, - boolean cancelOutdatedJobsEnabled + boolean cancelOutdatedJobsEnabled, + boolean approveOnBuildSuccessful, + boolean needsWorkOnBuildFailure ) throws ANTLRException { super(cron); this.projectPath = projectPath; @@ -113,6 +117,8 @@ public StashBuildTrigger( this.onlyBuildOnComment = onlyBuildOnComment; this.deletePreviousBuildFinishComments = deletePreviousBuildFinishComments; this.targetBranchesToBuild = targetBranchesToBuild; + this.approveOnBuildSuccessful = approveOnBuildSuccessful; + this.needsWorkOnBuildFailure = needsWorkOnBuildFailure; } public String getStashHost() { @@ -195,6 +201,14 @@ public boolean isCancelOutdatedJobsEnabled() { return cancelOutdatedJobsEnabled; } + public boolean isApproveOnBuildSuccessful() { + return approveOnBuildSuccessful; + } + + public boolean isNeedsWorkOnBuildFailure() { + return needsWorkOnBuildFailure; + } + @Override public void start(Job job, boolean newInstance) { try { diff --git a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuilds.java b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuilds.java index ca015277..eff65558 100644 --- a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuilds.java +++ b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuilds.java @@ -75,6 +75,16 @@ public void onCompleted(Run run, TaskListener listener) { cause.getDestinationCommitHash(), result, buildUrl, run.getNumber(), additionalComment, duration); + // Mark PR as Approved or Needs Work + StashMarkStatus status = new StashMarkStatus(); + status.handleStatus( + trigger.isApproveOnBuildSuccessful(), + trigger.isNeedsWorkOnBuildFailure(), + cause.getPullRequestId(), + run.getResult(), + repository + ); + //Merge PR StashBuildTrigger trig = StashBuildTrigger.getTrigger(run.getParent()); if(trig.getMergeOnSuccess() && run.getResult() == Result.SUCCESS) { diff --git a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashMarkStatus.java b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashMarkStatus.java new file mode 100644 index 00000000..73427729 --- /dev/null +++ b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashMarkStatus.java @@ -0,0 +1,21 @@ +package stashpullrequestbuilder.stashpullrequestbuilder; + +import hudson.model.Result; +import hudson.model.Run; + +/** + * Created by tariq on 12/04/2017. + */ +public class StashMarkStatus { + + public void handleStatus(Boolean approveOnBuildSuccessful, Boolean needsWorkOnBuildFailure, String pullRequestId, + Result result, StashRepository repository) { + if(approveOnBuildSuccessful && result == Result.SUCCESS) { + repository.markStatus(pullRequestId, "APPROVED"); + } + + if(needsWorkOnBuildFailure && result == Result.FAILURE) { + repository.markStatus(pullRequestId, "NEEDS_WORK"); + } + } +} diff --git a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashRepository.java b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashRepository.java index f728957c..b3002ae8 100644 --- a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashRepository.java +++ b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashRepository.java @@ -204,6 +204,10 @@ public boolean mergePullRequest(String pullRequestId, String version) return this.client.mergePullRequest(pullRequestId, version); } + public void markStatus(String pullRequestId, String status) { + this.client.markStatus(pullRequestId, status); + } + private Boolean isPullRequestMergable(StashPullRequestResponseValue pullRequest) { if (trigger.isCheckMergeable() || trigger.isCheckNotConflicted()) { StashPullRequestMergableResponse mergable = client.getPullRequestMergeStatus(pullRequest.getId()); diff --git a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashApiClient.java b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashApiClient.java index abc87adf..4bd4ec1b 100644 --- a/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashApiClient.java +++ b/src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashApiClient.java @@ -15,6 +15,7 @@ import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; @@ -66,11 +67,13 @@ public class StashApiClient { private String project; private String repositoryName; private Credentials credentials; + private String username; private boolean ignoreSsl; public StashApiClient(String stashHost, String username, String password, String project, String repositoryName, boolean ignoreSsl) { this.credentials = new UsernamePasswordCredentials(username, password); + this.username = username; this.project = project; this.repositoryName = repositoryName; this.apiBaseUrl = stashHost.replaceAll("/$", "") + "/rest/api/1.0/projects/"; @@ -171,6 +174,18 @@ public boolean mergePullRequest(String pullRequestId, String version) { return false; } + public void markStatus(String pullRequestId, String status) { + String path = pullRequestPath(pullRequestId) + "/participants/" + username; + + try { + putRequest(path, status); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (IOException e) { + logger.log(Level.SEVERE, "Failed to mark Stash PR status " + path + " " + e); + } + } + private HttpContext gethttpContext(Credentials credentials) { CredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials(AuthScope.ANY, credentials); @@ -430,6 +445,95 @@ public Callable init(HttpClient client, HttpPost httppost, HttpContext c return response; } + private String putRequest(String path, String status) throws UnsupportedEncodingException { + logger.log(Level.FINEST, "PR-PUT-REQUEST:" + path + " with: " + status); + HttpClient client = getHttpClient(); + HttpContext context = gethttpContext(credentials); + + HttpPut httpPut = new HttpPut(path); + //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html; section 14.10. + //tells the server that we want it to close the connection when it has sent the response. + //address large amount of close_wait sockets client and fin sockets server side + httpPut.setHeader("Connection", "close"); + httpPut.setHeader("X-Atlassian-Token", "no-check"); //xsrf + + if (status != null) { + ObjectNode node = mapper.getNodeFactory().objectNode(); + node.put("status", status); + StringEntity requestEntity = null; + try { + requestEntity = new StringEntity( + mapper.writeValueAsString(node), + ContentType.APPLICATION_JSON); + } catch (IOException e) { + e.printStackTrace(); + } + httpPut.setEntity(requestEntity); + } + + String response = ""; + FutureTask httpTask = null; + Thread thread; + + try { + //Run the http request in a future task so we have the opportunity + //to cancel it if it gets hung up; which is possible if stuck at + //socket native layer. see issue JENKINS-30558 + httpTask = new FutureTask(new Callable() { + + private HttpClient client; + private HttpContext context; + private HttpPut httpPut; + + @Override + public String call() throws Exception { + + HttpResponse httpResponse = client.execute(httpPut, context); + int responseCode = httpResponse.getStatusLine().getStatusCode(); + String response = httpResponse.getStatusLine().getReasonPhrase(); + if (!validResponseCode(responseCode)) { + logger.log(Level.SEVERE, "Failing to get response from Stash PR PUT" + httpPut.getURI().getPath()); + throw new RuntimeException("Didn't get a 200 response from Stash PR PUT! Response; '" + + responseCode + "' with message; " + response); + } + InputStream responseBodyAsStream = httpResponse.getEntity().getContent(); + StringWriter stringWriter = new StringWriter(); + IOUtils.copy(responseBodyAsStream, stringWriter, "UTF-8"); + response = stringWriter.toString(); + logger.log(Level.FINEST, "API Request Response: " + response); + + return response; + + } + + public Callable init(HttpClient client, HttpPut httpPut, HttpContext context) { + this.client = client; + this.context = context; + this.httpPut = httpPut; + return this; + } + + }.init(client, httpPut, context)); + thread = new Thread(httpTask); + thread.start(); + response = httpTask.get((long) StashApiClient.HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + } catch (TimeoutException e) { + e.printStackTrace(); + httpPut.abort(); + throw new RuntimeException(e); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } finally { + httpPut.releaseConnection(); + } + + logger.log(Level.FINEST, "PR-PUT-RESPONSE:" + response); + + return response; + } + private boolean validResponseCode(int responseCode) { return responseCode == HttpStatus.SC_OK || responseCode == HttpStatus.SC_ACCEPTED || diff --git a/src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger/config.jelly b/src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger/config.jelly index 8b53c34c..f5d5d7d3 100644 --- a/src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger/config.jelly +++ b/src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger/config.jelly @@ -49,5 +49,11 @@ + + + + + + diff --git a/src/test/java/stashpullrequestbuilder/stashpullrequestbuilder/StashMarkStatusTest.java b/src/test/java/stashpullrequestbuilder/stashpullrequestbuilder/StashMarkStatusTest.java new file mode 100644 index 00000000..e012e667 --- /dev/null +++ b/src/test/java/stashpullrequestbuilder/stashpullrequestbuilder/StashMarkStatusTest.java @@ -0,0 +1,58 @@ +package stashpullrequestbuilder.stashpullrequestbuilder; + +import hudson.model.Result; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.*; + +/** + * Created by tariq on 12/04/2017. + */ +public class StashMarkStatusTest { + + private StashRepository repository; + + @Before + public void setUp() throws Exception { + repository = mock(StashRepository.class); + } + + @Test + public void handleStatus_shouldMarkStatusApprovedOnSuccessfulBuild() throws Exception { + StashMarkStatus status = new StashMarkStatus(); + + status.handleStatus(true, false, "", Result.SUCCESS, repository); + + verify(repository).markStatus("", "APPROVED"); + } + + @Test + public void handleStatus_shouldMarkStatusNeedsWorkOnFailedBuild() throws Exception { + StashMarkStatus status = new StashMarkStatus(); + + status.handleStatus(false, true, "", Result.FAILURE, repository); + + verify(repository).markStatus("", "NEEDS_WORK"); + } + + @Test + public void handleStatus_shouldNotMarkStatusApprovedWhenDisabled() throws Exception { + StashMarkStatus status = new StashMarkStatus(); + + status.handleStatus(false, false, "", Result.SUCCESS, repository); + + verify(repository, never()).markStatus("", "APPROVED"); + } + + @Test + public void handleStatus_shouldNotMarkStatusNeedsWorkWhenDisabled() throws Exception { + StashMarkStatus status = new StashMarkStatus(); + + status.handleStatus(false, false, "", Result.FAILURE, repository); + + verify(repository, never()).markStatus("", "NEEDS_WORK"); + } + +} \ No newline at end of file