Skip to content

Commit 65b230e

Browse files
committed
Added options to mark a pull request as Approved or Needs Work
1 parent b2be679 commit 65b230e

File tree

9 files changed

+227
-2
lines changed

9 files changed

+227
-2
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,16 @@ Select *Stash Pull Request Builder* then configure:
6060
- Only build when asked (with test phrase):
6161
- CI Build Phrases: default: "test this please"
6262
- Target branches: a comma separated list of branches (e.g. brancha,branchb)
63+
- 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".
64+
- Mark PR with Needs Work on build failure: As above, but with failed builds. The same user requirements apply.
6365

6466
## Building the merge of Source Branch into Target Branch
6567

6668
You may want Jenkins to build the merged PR (that is the merge of `sourceBranch` into `targetBranch`) to catch any issues resulting from this. To do this change the Branch Specifier from `origin/pr/${pullRequestId}/from` to `origin/pr/${pullRequestId}/merge`
6769

6870
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.
6971

70-
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.
72+
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.
7173

7274
**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.
7375

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@
6666
<artifactId>credentials</artifactId>
6767
<version>2.1.5</version>
6868
</dependency>
69+
<dependency>
70+
<groupId>org.mockito</groupId>
71+
<artifactId>mockito-core</artifactId>
72+
<version>2.7.22</version>
73+
<scope>test</scope>
74+
</dependency>
6975
</dependencies>
7076

7177
<pluginRepositories>

src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public class StashBuildTrigger extends Trigger<Job<?, ?>> {
6969
private final boolean onlyBuildOnComment;
7070
private final boolean deletePreviousBuildFinishComments;
7171
private final boolean cancelOutdatedJobsEnabled;
72+
private final boolean approveOnBuildSuccessful;
73+
private final boolean needsWorkOnBuildFailure;
7274

7375
transient private StashPullRequestsBuilder stashPullRequestsBuilder;
7476

@@ -93,7 +95,9 @@ public StashBuildTrigger(
9395
String ciBuildPhrases,
9496
boolean deletePreviousBuildFinishComments,
9597
String targetBranchesToBuild,
96-
boolean cancelOutdatedJobsEnabled
98+
boolean cancelOutdatedJobsEnabled,
99+
boolean approveOnBuildSuccessful,
100+
boolean needsWorkOnBuildFailure
97101
) throws ANTLRException {
98102
super(cron);
99103
this.projectPath = projectPath;
@@ -113,6 +117,8 @@ public StashBuildTrigger(
113117
this.onlyBuildOnComment = onlyBuildOnComment;
114118
this.deletePreviousBuildFinishComments = deletePreviousBuildFinishComments;
115119
this.targetBranchesToBuild = targetBranchesToBuild;
120+
this.approveOnBuildSuccessful = approveOnBuildSuccessful;
121+
this.needsWorkOnBuildFailure = needsWorkOnBuildFailure;
116122
}
117123

118124
public String getStashHost() {
@@ -195,6 +201,14 @@ public boolean isCancelOutdatedJobsEnabled() {
195201
return cancelOutdatedJobsEnabled;
196202
}
197203

204+
public boolean isApproveOnBuildSuccessful() {
205+
return approveOnBuildSuccessful;
206+
}
207+
208+
public boolean isNeedsWorkOnBuildFailure() {
209+
return needsWorkOnBuildFailure;
210+
}
211+
198212
@Override
199213
public void start(Job<?, ?> job, boolean newInstance) {
200214
try {

src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuilds.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ public void onCompleted(Run run, TaskListener listener) {
7575
cause.getDestinationCommitHash(), result, buildUrl,
7676
run.getNumber(), additionalComment, duration);
7777

78+
// Mark PR as Approved or Needs Work
79+
StashMarkStatus status = new StashMarkStatus();
80+
status.handleStatus(
81+
trigger.isApproveOnBuildSuccessful(),
82+
trigger.isNeedsWorkOnBuildFailure(),
83+
cause.getPullRequestId(),
84+
run.getResult(),
85+
repository
86+
);
87+
7888
//Merge PR
7989
StashBuildTrigger trig = StashBuildTrigger.getTrigger(run.getParent());
8090
if(trig.getMergeOnSuccess() && run.getResult() == Result.SUCCESS) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package stashpullrequestbuilder.stashpullrequestbuilder;
2+
3+
import hudson.model.Result;
4+
import hudson.model.Run;
5+
6+
/**
7+
* Created by tariq on 12/04/2017.
8+
*/
9+
public class StashMarkStatus {
10+
11+
public void handleStatus(Boolean approveOnBuildSuccessful, Boolean needsWorkOnBuildFailure, String pullRequestId,
12+
Result result, StashRepository repository) {
13+
if(approveOnBuildSuccessful && result == Result.SUCCESS) {
14+
repository.markStatus(pullRequestId, "APPROVED");
15+
}
16+
17+
if(needsWorkOnBuildFailure && result == Result.FAILURE) {
18+
repository.markStatus(pullRequestId, "NEEDS_WORK");
19+
}
20+
}
21+
}

src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ public boolean mergePullRequest(String pullRequestId, String version)
204204
return this.client.mergePullRequest(pullRequestId, version);
205205
}
206206

207+
public void markStatus(String pullRequestId, String status) {
208+
this.client.markStatus(pullRequestId, status);
209+
}
210+
207211
private Boolean isPullRequestMergable(StashPullRequestResponseValue pullRequest) {
208212
if (trigger.isCheckMergeable() || trigger.isCheckNotConflicted()) {
209213
StashPullRequestMergableResponse mergable = client.getPullRequestMergeStatus(pullRequest.getId());

src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashApiClient.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.apache.http.client.methods.HttpDelete;
1616
import org.apache.http.client.methods.HttpGet;
1717
import org.apache.http.client.methods.HttpPost;
18+
import org.apache.http.client.methods.HttpPut;
1819
import org.apache.http.client.protocol.HttpClientContext;
1920
import org.apache.http.conn.ssl.NoopHostnameVerifier;
2021
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
@@ -66,11 +67,13 @@ public class StashApiClient {
6667
private String project;
6768
private String repositoryName;
6869
private Credentials credentials;
70+
private String username;
6971
private boolean ignoreSsl;
7072

7173

7274
public StashApiClient(String stashHost, String username, String password, String project, String repositoryName, boolean ignoreSsl) {
7375
this.credentials = new UsernamePasswordCredentials(username, password);
76+
this.username = username;
7477
this.project = project;
7578
this.repositoryName = repositoryName;
7679
this.apiBaseUrl = stashHost.replaceAll("/$", "") + "/rest/api/1.0/projects/";
@@ -171,6 +174,18 @@ public boolean mergePullRequest(String pullRequestId, String version) {
171174
return false;
172175
}
173176

177+
public void markStatus(String pullRequestId, String status) {
178+
String path = pullRequestPath(pullRequestId) + "/participants/" + username;
179+
180+
try {
181+
putRequest(path, status);
182+
} catch (UnsupportedEncodingException e) {
183+
e.printStackTrace();
184+
} catch (IOException e) {
185+
logger.log(Level.SEVERE, "Failed to mark Stash PR status " + path + " " + e);
186+
}
187+
}
188+
174189
private HttpContext gethttpContext(Credentials credentials) {
175190
CredentialsProvider credsProvider = new BasicCredentialsProvider();
176191
credsProvider.setCredentials(AuthScope.ANY, credentials);
@@ -430,6 +445,95 @@ public Callable<String> init(HttpClient client, HttpPost httppost, HttpContext c
430445
return response;
431446
}
432447

448+
private String putRequest(String path, String status) throws UnsupportedEncodingException {
449+
logger.log(Level.FINEST, "PR-PUT-REQUEST:" + path + " with: " + status);
450+
HttpClient client = getHttpClient();
451+
HttpContext context = gethttpContext(credentials);
452+
453+
HttpPut httpPut = new HttpPut(path);
454+
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html; section 14.10.
455+
//tells the server that we want it to close the connection when it has sent the response.
456+
//address large amount of close_wait sockets client and fin sockets server side
457+
httpPut.setHeader("Connection", "close");
458+
httpPut.setHeader("X-Atlassian-Token", "no-check"); //xsrf
459+
460+
if (status != null) {
461+
ObjectNode node = mapper.getNodeFactory().objectNode();
462+
node.put("status", status);
463+
StringEntity requestEntity = null;
464+
try {
465+
requestEntity = new StringEntity(
466+
mapper.writeValueAsString(node),
467+
ContentType.APPLICATION_JSON);
468+
} catch (IOException e) {
469+
e.printStackTrace();
470+
}
471+
httpPut.setEntity(requestEntity);
472+
}
473+
474+
String response = "";
475+
FutureTask<String> httpTask = null;
476+
Thread thread;
477+
478+
try {
479+
//Run the http request in a future task so we have the opportunity
480+
//to cancel it if it gets hung up; which is possible if stuck at
481+
//socket native layer. see issue JENKINS-30558
482+
httpTask = new FutureTask<String>(new Callable<String>() {
483+
484+
private HttpClient client;
485+
private HttpContext context;
486+
private HttpPut httpPut;
487+
488+
@Override
489+
public String call() throws Exception {
490+
491+
HttpResponse httpResponse = client.execute(httpPut, context);
492+
int responseCode = httpResponse.getStatusLine().getStatusCode();
493+
String response = httpResponse.getStatusLine().getReasonPhrase();
494+
if (!validResponseCode(responseCode)) {
495+
logger.log(Level.SEVERE, "Failing to get response from Stash PR PUT" + httpPut.getURI().getPath());
496+
throw new RuntimeException("Didn't get a 200 response from Stash PR PUT! Response; '" +
497+
responseCode + "' with message; " + response);
498+
}
499+
InputStream responseBodyAsStream = httpResponse.getEntity().getContent();
500+
StringWriter stringWriter = new StringWriter();
501+
IOUtils.copy(responseBodyAsStream, stringWriter, "UTF-8");
502+
response = stringWriter.toString();
503+
logger.log(Level.FINEST, "API Request Response: " + response);
504+
505+
return response;
506+
507+
}
508+
509+
public Callable<String> init(HttpClient client, HttpPut httpPut, HttpContext context) {
510+
this.client = client;
511+
this.context = context;
512+
this.httpPut = httpPut;
513+
return this;
514+
}
515+
516+
}.init(client, httpPut, context));
517+
thread = new Thread(httpTask);
518+
thread.start();
519+
response = httpTask.get((long) StashApiClient.HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
520+
521+
} catch (TimeoutException e) {
522+
e.printStackTrace();
523+
httpPut.abort();
524+
throw new RuntimeException(e);
525+
} catch (Exception e) {
526+
e.printStackTrace();
527+
throw new RuntimeException(e);
528+
} finally {
529+
httpPut.releaseConnection();
530+
}
531+
532+
logger.log(Level.FINEST, "PR-PUT-RESPONSE:" + response);
533+
534+
return response;
535+
}
536+
433537
private boolean validResponseCode(int responseCode) {
434538
return responseCode == HttpStatus.SC_OK ||
435539
responseCode == HttpStatus.SC_ACCEPTED ||

src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger/config.jelly

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,11 @@
4949
<f:entry title="CI Build Phrases" field="ciBuildPhrases">
5050
<f:textbox default="test this please"/>
5151
</f:entry>
52+
<f:entry title="Approve PR on build success" field="approveOnBuildSuccessful">
53+
<f:checkbox default="false"/>
54+
</f:entry>
55+
<f:entry title="Mark PR with Needs Work on build failure" field="needsWorkOnBuildFailure">
56+
<f:checkbox default="false"/>
57+
</f:entry>
5258
</f:advanced>
5359
</j:jelly>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package stashpullrequestbuilder.stashpullrequestbuilder;
2+
3+
import hudson.model.Result;
4+
5+
import org.junit.Before;
6+
import org.junit.Test;
7+
8+
import static org.mockito.Mockito.*;
9+
10+
/**
11+
* Created by tariq on 12/04/2017.
12+
*/
13+
public class StashMarkStatusTest {
14+
15+
private StashRepository repository;
16+
17+
@Before
18+
public void setUp() throws Exception {
19+
repository = mock(StashRepository.class);
20+
}
21+
22+
@Test
23+
public void handleStatus_shouldMarkStatusApprovedOnSuccessfulBuild() throws Exception {
24+
StashMarkStatus status = new StashMarkStatus();
25+
26+
status.handleStatus(true, false, "", Result.SUCCESS, repository);
27+
28+
verify(repository).markStatus("", "APPROVED");
29+
}
30+
31+
@Test
32+
public void handleStatus_shouldMarkStatusNeedsWorkOnFailedBuild() throws Exception {
33+
StashMarkStatus status = new StashMarkStatus();
34+
35+
status.handleStatus(false, true, "", Result.FAILURE, repository);
36+
37+
verify(repository).markStatus("", "NEEDS_WORK");
38+
}
39+
40+
@Test
41+
public void handleStatus_shouldNotMarkStatusApprovedWhenDisabled() throws Exception {
42+
StashMarkStatus status = new StashMarkStatus();
43+
44+
status.handleStatus(false, false, "", Result.SUCCESS, repository);
45+
46+
verify(repository, never()).markStatus("", "APPROVED");
47+
}
48+
49+
@Test
50+
public void handleStatus_shouldNotMarkStatusNeedsWorkWhenDisabled() throws Exception {
51+
StashMarkStatus status = new StashMarkStatus();
52+
53+
status.handleStatus(false, false, "", Result.FAILURE, repository);
54+
55+
verify(repository, never()).markStatus("", "NEEDS_WORK");
56+
}
57+
58+
}

0 commit comments

Comments
 (0)