Bitbucket auto merge pull requests when conditions are met

Rasmus Steiniche July 20, 2016

Hello,

 

I am working on making our pull requests merge automatically if a certain set of conditions are true.

However, I have not been successful yet and therefore kindly ask for your help!

I am using the adaptavist ScriptRunner for Bitbucket version 4.5.2 (Self-hosted Bitbucket).

By testing I have figured out that a merge, triggered from adaptavist ScriptRunner, can only happen if the pull request "conditions" are met.
In my case:
✓ Requires 1 approvers
✓ Requires all tasks to be resolved 
✓ Requires a minimum of 1 successful builds 

It seems like build status and issue state is not part of the pull request? I do not know if this is true? But I can not find any information about e.g. build statuses in the documentation here: https://developer.atlassian.com/static/javadoc/bitbucket-server/4.5.2/api/reference/com/atlassian/bitbucket/event/pull/package-summary.html

I can make the adaptavist ScriptRunner fire events e.g. in case of approval with the following code:

event.pullRequest.reviewers.count {it.approved} >= 1


However if the tasks are not resolved or there are no successful builds the merge will not happen (This is as it should be!)
But when tasks do get resolved or a successful build comes in I cannot get the adaptavist ScriptRunner to trigger, I have tried with the following code:

event.pullRequest

If my suspicions is true and Bitbucket only handles pull requests events which are defined in git this means that I have no way to check for these events with the adaptavist ScriptRunner right?

Some elegant solution for me would be to make Bitbucket and adaptavist ScriptRunner able to trigger on all events on a pull request, i.e. issue state change in jira, build status from CI, approvals.
Or make a scheduler which checks every 10 seconds if something is ready to be merged. 

I have heard about a workaround where the CI server tries to merge the pull requests if the conditions are met, but I think this is a far from optimal solution.

If anymore information is needed feel free to ask! 

Anyone have any information or helpful thoughts about how I can get this to work?

1 answer

1 vote
adammarkham
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 11, 2016

This question took a long time to find. In the future could you add the tag "com.onresolve.jira.stash.groovyrunner" so we can answer more quickly. I have added it for you now.

Your correct the pull request does not hold all this information, even if it did it wouldn't trigger with the auto merge built-in event handler, thats only triggered on a pull request approval. You will need to do a custom event listener.

This is not trivial to do for a number of reasons. The first being that not all events are triggered in Bitbucket, some are in JIRA also.

Although I have got a partial solution for you which comprises of an event listener which is triggered on a pull request approval which then uses an application link between JIRA and Bitbucket to get issue statuses and the BuildStatusService to get the build status

First you need a custom event handler script, adding this an the inline script for events, PullRequestApprovedEvent:

import com.atlassian.bitbucket.build.BuildStatusService
import com.atlassian.bitbucket.event.pull.PullRequestApprovedEvent
import com.atlassian.bitbucket.integration.jira.JiraIssueService
import com.atlassian.bitbucket.pull.PullRequest
import com.atlassian.bitbucket.pull.PullRequestMergeRequest
import com.atlassian.bitbucket.pull.PullRequestService
import com.atlassian.bitbucket.user.SecurityService
import com.atlassian.bitbucket.util.Operation
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.sal.api.net.Request
import com.atlassian.sal.api.net.Response
import com.atlassian.sal.api.net.ResponseException
import com.atlassian.sal.api.net.ReturningResponseHandler
import com.onresolve.scriptrunner.canned.bitbucket.util.BitbucketBaseScript
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import groovy.transform.BaseScript

//✓ Requires 1 approvers
//✓ Requires all tasks to be resolved
//✓ Requires a minimum of 1 successful build

@BaseScript BitbucketBaseScript baseScript

def buildStatusService = ComponentLocator.getComponent(BuildStatusService)
def jiraIssueService = ComponentLocator.getComponent(JiraIssueService)
def securityService = ComponentLocator.getComponent(SecurityService)
def pullRequestService = ComponentLocator.getComponent(PullRequestService)

def pullRequests = []
def repository = null

if (event instanceof PullRequestApprovedEvent) {
    def event = event as PullRequestApprovedEvent

    pullRequests.add(event.pullRequest)
}
 
// we need to gather the pull requests for each event using above approach

// go through each pull request and check conditions
pullRequests.each { pullRequest ->
    // require 1 approver
    if (pullRequest.participants.find { it.approved }) {
        def summaries = buildStatusService.getSummaries(pullRequest.getCommits()*.id)

        def successfulBuild = summaries.values().find { it.successfulCount > 0 }

        // require 1 successful build
        if (successfulBuild) {

            def issueKeys = jiraIssueService.getIssuesForPullRequest(repository, pullRequest.id)*.key

            def jiraLink = getJiraAppLink()

            def authenticatedRequestFactory = jiraLink.createImpersonatingAuthenticatedRequestFactory()

            def allIssuesResolved = issueKeys.every { String key ->
                def response = authenticatedRequestFactory
                    .createRequest(Request.MethodType.GET, "/rest/api/2/issue/$key?fields=status")
                    .addHeader("Content-Type", "application/json")
                    .executeAndReturn(new ReturningResponseHandler<Response, Map>() {
                    @Override
                    Map handle(Response response) throws ResponseException {
                        if (response.statusCode != HttpURLConnection.HTTP_OK) {
                            throw new Exception(response.getResponseBodyAsString())
                        }

                        return response.getEntity(Map)
                    }
                })

                response.fields.status.name == "Done"
            }

            // require all issues to be resolved - this doesn't handle subtasks or linked issues
            if (allIssuesResolved) {
                mergePullRequest(securityService, pullRequest, pullRequestService)
            }
        }
    }
}

private Object mergePullRequest(SecurityService securityService, PullRequest pullRequest, pullRequestService) {
    securityService.impersonating(pullRequest.author.user, "Merging pull-request on behalf of author").call(new Operation<Object, RuntimeException>() {
        @SuppressWarnings("ConstantConditions")
        @Override
        public Object perform() {
            pullRequestService.merge(new PullRequestMergeRequest.Builder(pullRequest).message("Automatically merged").build())
        }
    })
}

This would be more optimal than having a service poll every few minutes.Unfortunately this event handler won't fire when all the subtasks are resolved in JIRA or when the build status changes for the pull request. I haven't quite got a solution for that currently. I will update this when I have, but this should get you going.

At a high level, maybe consider adding a REST endpoint in Bitbucket and calling it from your build server once the build is complete and successful, then publishing an event from that endpoint inside Bitbucket which will fire the above listener. Also could be the same if you add a scripted post-function on a JIRA workflow transition to signal when an issue is done and call an endpoint in Bitbucket.

You can publish events using the code below:

def eventPublisher = ComponentLocator.getComponent(EventPublisher)
 
// change for your event
eventPublisher.publish(new RepositoryPushEvent("", repository, []))

Its just mapping the build back to the actual pull request which is tricky.

Hope this helps,
Adam 

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events