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