Skip to content

Added options to mark a pull request as Approved or Needs Work #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ 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

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`

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.

Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
<artifactId>credentials</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.7.22</version>
<scope>test</scope>
</dependency>
</dependencies>

<pluginRepositories>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public class StashBuildTrigger extends Trigger<Job<?, ?>> {
private final boolean onlyBuildOnComment;
private final boolean deletePreviousBuildFinishComments;
private final boolean cancelOutdatedJobsEnabled;
private final boolean approveOnBuildSuccessful;
private final boolean needsWorkOnBuildFailure;

transient private StashPullRequestsBuilder stashPullRequestsBuilder;

Expand All @@ -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;
Expand All @@ -113,6 +117,8 @@ public StashBuildTrigger(
this.onlyBuildOnComment = onlyBuildOnComment;
this.deletePreviousBuildFinishComments = deletePreviousBuildFinishComments;
this.targetBranchesToBuild = targetBranchesToBuild;
this.approveOnBuildSuccessful = approveOnBuildSuccessful;
this.needsWorkOnBuildFailure = needsWorkOnBuildFailure;
}

public String getStashHost() {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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/";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -430,6 +445,95 @@ public Callable<String> 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<String> 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<String>(new Callable<String>() {

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<String> 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 ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,11 @@
<f:entry title="CI Build Phrases" field="ciBuildPhrases">
<f:textbox default="test this please"/>
</f:entry>
<f:entry title="Approve PR on build success" field="approveOnBuildSuccessful">
<f:checkbox default="false"/>
</f:entry>
<f:entry title="Mark PR with Needs Work on build failure" field="needsWorkOnBuildFailure">
<f:checkbox default="false"/>
</f:entry>
</f:advanced>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -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");
}

}