diff options
10 files changed, 199 insertions, 38 deletions
@@ -37,6 +37,19 @@ Creating a Job - Write RepositoryName - Save to preserve your changes +Merge the Pull Request's Source Branch into the Target Branch Before Building +============================================================================== +You may want Jenkins to attempt to merge your PR before doing the build -- this way it will find conflicts for you automatically. +- Follow the steps above in "Creating a Job" +- In the "Source Code Management" > "Git" > "Additional Behaviors" section, click "Add" > "Merge Before Building" +- In "Name of Repository" put "origin" (or, if not using default name, use your remote repository's name. Note: unlike in the main part of the Git Repository config, you cannot leave this item blank for "default".) +- In "Branch to merge to" put "${targetBranch}" +- Note that as long as you don't push these changes to your remote repository, the merge only happens in your local repository. + + +If you are merging into your target branch, you might want Jenkins to do a new build of the Pull Request when the target branch changes. +- There is a checkbox that says, "Rebuild if destination branch changes?" which enables this check. + Rerun test builds ==================== @@ -8,7 +8,7 @@ <artifactId>bitbucket-pullrequest-builder</artifactId> <name>Bitbucket Pullrequest Builder Plugin</name> - <version>1.4.2-SNAPSHOT</version> + <version>1.4.6-SNAPSHOT</version> <description>This Jenkins plugin builds pull requests from Bitbucket.org and will report the test results.</description> <packaging>hpi</packaging> <url>https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+pullrequest+builder+plugin</url> diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java index af4e98c..7b72562 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java @@ -29,6 +29,9 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { private final String repositoryOwner; private final String repositoryName; private final String ciSkipPhrases; + private final boolean checkDestinationCommit; + private final boolean approveIfSuccess; + transient private BitbucketPullRequestsBuilder bitbucketPullRequestsBuilder; @Extension @@ -42,7 +45,10 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { String password, String repositoryOwner, String repositoryName, - String ciSkipPhrases) throws ANTLRException { + String ciSkipPhrases, + boolean checkDestinationCommit, + boolean approveIfSuccess + ) throws ANTLRException { super(cron); this.projectPath = projectPath; this.cron = cron; @@ -51,6 +57,8 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { this.repositoryOwner = repositoryOwner; this.repositoryName = repositoryName; this.ciSkipPhrases = ciSkipPhrases; + this.checkDestinationCommit = checkDestinationCommit; + this.approveIfSuccess = approveIfSuccess; } public String getProjectPath() { @@ -80,6 +88,14 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { public String getCiSkipPhrases() { return ciSkipPhrases; } + + public boolean getCheckDestinationCommit() { + return checkDestinationCommit; + } + + public boolean getApproveIfSuccess() { + return approveIfSuccess; + } @Override public void start(AbstractProject<?, ?> project, boolean newInstance) { @@ -109,7 +125,7 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { values.put("sourceBranch", new StringParameterValue("sourceBranch", cause.getSourceBranch())); values.put("targetBranch", new StringParameterValue("targetBranch", cause.getTargetBranch())); values.put("repositoryOwner", new StringParameterValue("repositoryOwner", cause.getRepositoryOwner())); - values.put("repositonyName", new StringParameterValue("repositoryName", cause.getRepositoryName())); + values.put("repositoryName", new StringParameterValue("repositoryName", cause.getRepositoryName())); values.put("pullRequestId", new StringParameterValue("pullRequestId", cause.getPullRequestId())); values.put("destinationRepositoryOwner", new StringParameterValue("destinationRepositoryOwner", cause.getDestinationRepositoryOwner())); values.put("destinationRepositoryName", new StringParameterValue("destinationRepositoryName", cause.getDestinationRepositoryName())); diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java index 9600e26..45e1873 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java @@ -57,6 +57,9 @@ public class BitbucketBuilds { buildUrl = rootUrl + build.getUrl(); } repository.deletePullRequestComment(cause.getPullRequestId(), cause.getBuildStartCommentId()); - repository.postFinishedComment(cause.getPullRequestId(), cause.getCommitHash(), result == Result.SUCCESS, buildUrl); + repository.postFinishedComment(cause.getPullRequestId(), cause.getSourceCommitHash(), cause.getDestinationCommitHash(), result == Result.SUCCESS, buildUrl); + if ( this.trigger.getApproveIfSuccess() && result == Result.SUCCESS ) { + this.repository.postPullRequestApproval(cause.getPullRequestId()); + } } } diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java index 2bdc558..01e6cd1 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java @@ -14,7 +14,8 @@ public class BitbucketCause extends Cause { private final String destinationRepositoryOwner; private final String destinationRepositoryName; private final String pullRequestTitle; - private final String commitHash; + private final String sourceCommitHash; + private final String destinationCommitHash; private final String buildStartCommentId; public static final String BITBUCKET_URL = "https://bitbucket.org/"; @@ -26,7 +27,8 @@ public class BitbucketCause extends Cause { String destinationRepositoryOwner, String destinationRepositoryName, String pullRequestTitle, - String commitHash, + String sourceCommitHash, + String destinationCommitHash, String buildStartCommentId) { this.sourceBranch = sourceBranch; this.targetBranch = targetBranch; @@ -36,7 +38,8 @@ public class BitbucketCause extends Cause { this.destinationRepositoryOwner = destinationRepositoryOwner; this.destinationRepositoryName = destinationRepositoryName; this.pullRequestTitle = pullRequestTitle; - this.commitHash = commitHash; + this.sourceCommitHash = sourceCommitHash; + this.destinationCommitHash = destinationCommitHash; this.buildStartCommentId = buildStartCommentId; } @@ -72,15 +75,17 @@ public class BitbucketCause extends Cause { return pullRequestTitle; } - public String getCommitHash() { return commitHash; } + public String getSourceCommitHash() { return sourceCommitHash; } + + public String getDestinationCommitHash() { return destinationCommitHash; } public String getBuildStartCommentId() { return buildStartCommentId; } @Override public String getShortDescription() { - String description = "<a href=" + BITBUCKET_URL + this.getDestinationRepositoryOwner() + "/"; + String description = "<a href=\"" + BITBUCKET_URL + this.getDestinationRepositoryOwner() + "/"; description += this.getDestinationRepositoryName() + "/pull-request/" + this.getPullRequestId(); - description += ">#" + this.getPullRequestId() + " " + this.getPullRequestTitle() + "</a>"; + description += "\">#" + this.getPullRequestId() + " " + this.getPullRequestTitle() + "</a>"; return description; } } diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java index 61d3702..43e4ddd 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java @@ -1,28 +1,34 @@ package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BitbucketApiClient; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BitbucketPullRequestComment; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BitbucketPullRequestResponseValue; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BitbucketPullRequestResponseValueRepository; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BitbucketApiClient; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BitbucketPullRequestComment; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BitbucketPullRequestResponseValue; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BitbucketPullRequestResponseValueRepository; /** * Created by nishio */ public class BitbucketRepository { private static final Logger logger = Logger.getLogger(BitbucketRepository.class.getName()); - public static final String BUILD_START_MARKER = "[*BuildStarted*] %s"; - public static final String BUILD_FINISH_MARKER = "[*BuildFinished*] %s"; + public static final String BUILD_START_MARKER = "[*BuildStarted* **%s**] %s into %s"; + public static final String BUILD_FINISH_MARKER = "[*BuildFinished* **%s**] %s into %s"; + + public static final String BUILD_START_REGEX = "\\[\\*BuildStarted\\* \\*\\*%s\\*\\*\\] ([0-9a-fA-F]+) into ([0-9a-fA-F]+)"; + public static final String BUILD_FINISH_REGEX = "\\[\\*BuildFinished\\* \\*\\*%s\\*\\*\\] ([0-9a-fA-F]+) into ([0-9a-fA-F]+)"; + public static final String BUILD_FINISH_SENTENCE = BUILD_FINISH_MARKER + " \n\n **%s** - %s"; public static final String BUILD_REQUEST_MARKER = "test this please"; - public static final String BUILD_SUCCESS_COMMENT = ":star:SUCCESS"; - public static final String BUILD_FAILURE_COMMENT = ":x:FAILURE"; + public static final String BUILD_SUCCESS_COMMENT = "✔ SUCCESS"; + public static final String BUILD_FAILURE_COMMENT = "✖ FAILURE"; private String projectPath; private BitbucketPullRequestsBuilder builder; private BitbucketBuildTrigger trigger; @@ -55,8 +61,9 @@ public class BitbucketRepository { } public String postBuildStartCommentTo(BitbucketPullRequestResponseValue pullRequest) { - String commit = pullRequest.getSource().getCommit().getHash(); - String comment = String.format(BUILD_START_MARKER, commit); + String sourceCommit = pullRequest.getSource().getCommit().getHash(); + String destinationCommit = pullRequest.getDestination().getCommit().getHash(); + String comment = String.format(BUILD_START_MARKER, builder.getProject().getDisplayName(), sourceCommit, destinationCommit); BitbucketPullRequestComment commentResponse = this.client.postPullRequestComment(pullRequest.getId(), comment); return commentResponse.getCommentId().toString(); } @@ -64,6 +71,9 @@ public class BitbucketRepository { public void addFutureBuildTasks(Collection<BitbucketPullRequestResponseValue> pullRequests) { for(BitbucketPullRequestResponseValue pullRequest : pullRequests) { String commentId = postBuildStartCommentTo(pullRequest); + if ( this.trigger.getApproveIfSuccess() ) { + deletePullRequestApproval(pullRequest.getId()); + } BitbucketCause cause = new BitbucketCause( pullRequest.getSource().getBranch().getName(), pullRequest.getDestination().getBranch().getName(), @@ -74,6 +84,7 @@ public class BitbucketRepository { pullRequest.getDestination().getRepository().getRepositoryName(), pullRequest.getTitle(), pullRequest.getSource().getCommit().getHash(), + pullRequest.getDestination().getCommit().getHash(), commentId); this.builder.getTrigger().startJob(cause); } @@ -83,46 +94,84 @@ public class BitbucketRepository { this.client.deletePullRequestComment(pullRequestId,commentId); } - public void postFinishedComment(String pullRequestId, String commit, boolean success, String buildUrl) { + public void postFinishedComment(String pullRequestId, String sourceCommit, String destinationCommit, boolean success, String buildUrl) { String message = BUILD_FAILURE_COMMENT; if (success){ message = BUILD_SUCCESS_COMMENT; } - String comment = String.format(BUILD_FINISH_SENTENCE, commit, message, buildUrl); + String comment = String.format(BUILD_FINISH_SENTENCE, builder.getProject().getDisplayName(), sourceCommit, destinationCommit, message, buildUrl); this.client.postPullRequestComment(pullRequestId, comment); } + public void deletePullRequestApproval(String pullRequestId) { + this.client.deletePullRequestApproval(pullRequestId); + } + + public void postPullRequestApproval(String pullRequestId) { + this.client.postPullRequestApproval(pullRequestId); + } + private boolean isBuildTarget(BitbucketPullRequestResponseValue pullRequest) { + boolean shouldBuild = true; if (pullRequest.getState() != null && pullRequest.getState().equals("OPEN")) { if (isSkipBuild(pullRequest.getTitle())) { return false; } - String commit = pullRequest.getSource().getCommit().getHash(); + String sourceCommit = pullRequest.getSource().getCommit().getHash(); + BitbucketPullRequestResponseValueRepository destination = pullRequest.getDestination(); String owner = destination.getRepository().getOwnerName(); String repositoryName = destination.getRepository().getRepositoryName(); + String destinationCommit = destination.getCommit().getHash(); + String id = pullRequest.getId(); List<BitbucketPullRequestComment> comments = client.getPullRequestComments(owner, repositoryName, id); - String searchStartMarker = String.format(BUILD_START_MARKER, commit).toLowerCase(); - String searchFinishMarker = String.format(BUILD_FINISH_MARKER, commit).toLowerCase(); if (comments != null) { Collections.sort(comments); Collections.reverse(comments); - for(BitbucketPullRequestComment comment : comments) { + for (BitbucketPullRequestComment comment : comments) { String content = comment.getContent(); if (content == null || content.isEmpty()) { continue; } - content = content.toLowerCase(); - if (content.contains(searchStartMarker) || - content.contains(searchFinishMarker)) { - shouldBuild = false; - break; + + //These will match any start or finish message -- need to check commits + String project_build_start = String.format(BUILD_START_REGEX, builder.getProject().getDisplayName()); + String project_build_finished = String.format(BUILD_FINISH_REGEX, builder.getProject().getDisplayName()); + Matcher startMatcher = Pattern.compile(project_build_start, Pattern.CASE_INSENSITIVE).matcher(content); + Matcher finishMatcher = Pattern.compile(project_build_finished, Pattern.CASE_INSENSITIVE).matcher(content); + + if (startMatcher.find() || + finishMatcher.find()) { + + String sourceCommitMatch; + String destinationCommitMatch; + + if (startMatcher.find(0)) { + sourceCommitMatch = startMatcher.group(1); + destinationCommitMatch = startMatcher.group(2); + } else { + sourceCommitMatch = finishMatcher.group(1); + destinationCommitMatch = finishMatcher.group(2); + } + + //first check source commit -- if it doesn't match, just move on. If it does, investigate further. + if (sourceCommitMatch.equalsIgnoreCase(sourceCommit)) { + // if we're checking destination commits, and if this doesn't match, then move on. + if (this.trigger.getCheckDestinationCommit() + && (!destinationCommitMatch.equalsIgnoreCase(destinationCommit))) { + continue; + } + + shouldBuild = false; + break; + } } + if (content.contains(BUILD_REQUEST_MARKER.toLowerCase())) { shouldBuild = true; break; diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BitbucketApiClient.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BitbucketApiClient.java index af43408..71fc861 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BitbucketApiClient.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BitbucketApiClient.java @@ -10,10 +10,14 @@ import org.codehaus.jackson.type.TypeReference; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; +import hudson.ProxyConfiguration; + /** * Created by nishio */ @@ -39,7 +43,7 @@ public class BitbucketApiClient { } catch(Exception e) { logger.log(Level.WARNING, "invalid pull request response.", e); } - return null; + return Collections.EMPTY_LIST; } public List<BitbucketPullRequestComment> getPullRequestComments(String commentOwnerName, String commentRepositoryName, String pullRequestId) { @@ -50,7 +54,7 @@ public class BitbucketApiClient { } catch(Exception e) { logger.log(Level.WARNING, "invalid pull request response.", e); } - return null; + return Collections.EMPTY_LIST; } public void deletePullRequestComment(String pullRequestId, String commentId) { @@ -76,8 +80,44 @@ public class BitbucketApiClient { return null; } - private String getRequest(String path) { + public void deletePullRequestApproval(String pullRequestId) { + String path = V2_API_BASE_URL + this.owner + "/" + this.repositoryName + "/pullrequests/" + pullRequestId + "/approve"; + deleteRequest(path); + } + + public BitbucketPullRequestApproval postPullRequestApproval(String pullRequestId) { + String path = V2_API_BASE_URL + this.owner + "/" + this.repositoryName + "/pullrequests/" + pullRequestId + "/approve"; + try { + String response = postRequest(path, new NameValuePair[]{}); + return parseApprovalJson(response); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + private HttpClient getHttpClient() { HttpClient client = new HttpClient(); + if (Jenkins.getInstance() != null) { + ProxyConfiguration proxy = Jenkins.getInstance().proxy; + if (proxy != null) { + logger.info("Jenkins proxy: " + proxy.name + ":" + proxy.port); + client.getHostConfiguration().setProxy(proxy.name, proxy.port); + String username = proxy.getUserName(); + String password = proxy.getPassword(); + // Consider it to be passed if username specified. Sufficient? + if (username != null && !"".equals(username.trim())) { + logger.info("Using proxy authentication (user=" + username + ")"); + client.getState().setProxyCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(username, password)); + } + } + } + return client; + } + + private String getRequest(String path) { + HttpClient client = getHttpClient(); client.getState().setCredentials(AuthScope.ANY, credentials); GetMethod httpget = new GetMethod(path); client.getParams().setAuthenticationPreemptive(true); @@ -94,7 +134,7 @@ public class BitbucketApiClient { } public void deleteRequest(String path) { - HttpClient client = new HttpClient(); + HttpClient client = getHttpClient(); client.getState().setCredentials(AuthScope.ANY, credentials); DeleteMethod httppost = new DeleteMethod(path); client.getParams().setAuthenticationPreemptive(true); @@ -107,7 +147,7 @@ public class BitbucketApiClient { } private String postRequest(String path, NameValuePair[] params) throws UnsupportedEncodingException { - HttpClient client = new HttpClient(); + HttpClient client = getHttpClient(); client.getState().setCredentials(AuthScope.ANY, credentials); PostMethod httppost = new PostMethod(path); httppost.setRequestBody(params); @@ -151,5 +191,14 @@ public class BitbucketApiClient { BitbucketPullRequestComment.class); return parsedResponse; } + + private BitbucketPullRequestApproval parseApprovalJson(String response) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + BitbucketPullRequestApproval parsedResponse; + parsedResponse = mapper.readValue( + response, + BitbucketPullRequestApproval.class); + return parsedResponse; + } } diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BitbucketPullRequestApproval.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BitbucketPullRequestApproval.java new file mode 100644 index 0000000..a7fe813 --- /dev/null +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BitbucketPullRequestApproval.java @@ -0,0 +1,20 @@ +package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.annotate.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class BitbucketPullRequestApproval { + private String role; + private Boolean approved; + + @JsonProperty("role") + public String getRole() { + return role; + } + + @JsonProperty("approved") + public Boolean getApproved() { + return approved; + } +} diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly index da12a64..0eee781 100644 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly +++ b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly @@ -17,4 +17,10 @@ <f:entry title="CI Skip Phrases" field="ciSkipPhrases"> <f:textbox /> </f:entry> + <f:entry title="Rebuild if destination branch changes?" field="checkDestinationCommit"> + <f:checkbox /> + </f:entry> + <f:entry title="Approve if build success?" field="approveIfSuccess"> + <f:checkbox /> + </f:entry> </j:jelly>
\ No newline at end of file diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly index 538998f..0404451 100644 --- a/src/main/resources/index.jelly +++ b/src/main/resources/index.jelly @@ -2,5 +2,5 @@ This view is used to render the installed plugins page. --> <div> - This plugin is a sample to explain how to write a Jenkins plugin. + This plugin polls BitBucket to determine whether there are Pull Requests that should be built. </div> |