ScriptRunner allows you to quickly write merge checks.

Merge checks are used to prevent pull requests from being merged, until your required conditions are met. Example of uses for merge checks:

Adding a Merge Check

Navigate to Admin → Script Merge Checks. Click a heading to add a handler. Choose Custom Merge Check to use your own scripts to decide whether to allow the merge or not.

Built-in Merge Checks

Require a Minimum Number of Approvers

This simply requires that some provided number of reviewers have approved the pull request. This is already available on a per-repo basis, where you can require that pull requests are reviewed by, say, two reviewers. But, what makes this powerful is that you can combine this with see conditions to create a powerful git workflow. Examples:

  • Requiring additional reviewers when the changed files includes sensitive code

  • Requiring fewer reviewers when the target is a long-term feature branch

mergeRequest.pullRequest.toRef.displayId == "long-term-feature"
  • Requiring more reviewers when the source is on a hotfix branch, and the target branch is your release branch

mergeRequest.pullRequest.fromRef.displayId.matches("hotfix/.*") &&
    mergeRequest.pullRequest.toRef.displayId == "release"

Require That a Pull Request Is Associated with a Valid Jira Issue

This requires the pull request to reference a valid Jira issue, where the supplied JQL query defines what’s valid. The issue can either be mentioned in the pull request title, or be present in one of the commit messages comprising the pull request. See also the version that works on push.

A configured JIRA application link is required. You either have to make one of your application links primary or select one when configuring this hook, as in the image below. If you select multiple application links then each one will be queried until one returns issues which match the specified JQL clause.
jira app link select
If you remove an application link you have selected for the hook, you should also update the hook configuration. As the hook verifies the application link can be found. If you don’t the hook will fail when you push to it.

A sample query might be:

  • The issue is "in progress" and assigned to the user attempting to merge the pull request:

status = "In Progress" and assignee = currentUser()
  • You could also require that the pull request references a particular Jira project:

project = FOO

If the merge check doesn’t allow the merge, the user will need to edit the pull request title, or rebase and push.

You can also build a dynamic JQL clause to query on. You can either create your own templates or use one of the samples that are included in the default installation of ScriptRunner.

Can only merge to a valid release branch

The following JQL clause template ensures that the target branch name corresponds to a fix version in Jira before merging.

fixVersion = '${mergeRequest.pullRequest.toRef.displayId}'

This enforces that any changes can be traced back to a particular release through Jira.

Require each commit to be associated with a valid Jira issue

It may be desirable for each commit to be associated with at least one Jira issue, this can be useful for tracking all commits back to a Jira issue.

This restriction can be optionally enabled for the merge check by checking the Require Valid Jira Issue in Every Commit? checkbox. With this checkbox checked, the merge of the pull request will be blocked if any commit exists without at least one valid Jira issue referenced in the commit message.

Conditional Merge Check

Allows you to specify a condition, which when evaluated to 'true' will cause a merge of the pull request to be vetoed.

Here is an example condition which will block a pull request merge if one of the reviewers marks the pull request as needs more work. You can find this example if you click "expand examples" under the condition field.

import com.atlassian.bitbucket.pull.PullRequestParticipantStatus

def reviewers = mergeRequest.pullRequest.reviewers

reviewers.any { it.status == PullRequestParticipantStatus.NEEDS_WORK }

Also note, that when specifying a conditional merge check, a custom 'veto message' can also be specified, which will be displayed to the user, whos merge is being prevented.

Working with Custom Merge Checks

Custom merge checks should return a RepositoryHookResult

To block the merge you should call the veto method on RepositoryHookResult and return the RepositoryHookResult returned by the method.

import com.atlassian.bitbucket.hook.repository.RepositoryHookResult

RepositoryHookResult.rejected("You cannot merge", "A more detailed explanation of why you can't merge")

The same functionality can now be achieved using a 'Conditional Merge Check' documented above.


Enforcing reviewers in several timezones

The intention of this hook is to ensure that the author and reviewers are in at least three time zones, as a mechanism of ensuring people in different offices are aware of code changes.

This approach is used by the Microsoft Kinect team apparently.

Currently we speak to Jira to get the timezone of the participants. When STASH-2817 is fixed this can be done without recourse to Jira. We use the application link to speak to Jira, and cache the results for future usage.

def final nTimeZonesRequired = 3

@BaseScript BitbucketBaseScript baseScript
MergeRequest mergeRequest = mergeRequest

Set participants = mergeRequest.pullRequest.reviewers*.user
participants <<

 * Get list of timezones for a bunch of user names, from JIRA. As this is run every time the PR page
 * is shown, we cache the result
 * @param jiraLink
 * @param participants
 * @return list of timezones
@Memoized(maxCacheSize = 100)
static List<String> getTimeZonesForUsers(ApplicationLink jiraLink, Set<String> participants) {

    def authenticatedRequestFactory = jiraLink.createImpersonatingAuthenticatedRequestFactory()
    def log = Logger.getLogger(this.class)
    def timeZones = participants.findResults { key ->
            .createRequest(Request.MethodType.GET, "rest/api/2/user?username=$key")
            .addHeader("Content-Type", "application/json")
                handle: { Response response ->
                    if (response.successful) {
                        new JsonSlurper().parseText(response.responseBodyAsString).timeZone
                    } else {
                        log.warn "Failed to look up user $key in JIRA: " + response.responseBodyAsString
                }] as ApplicationLinkResponseHandler<Void>

def timeZones = getTimeZonesForUsers(getJiraAppLink(), participants*.name as Set)

def actualTimeZones = timeZones.unique().size()
if (actualTimeZones < nTimeZonesRequired) {
    RepositoryHookResult.rejected("Not enough timezones covered",
        "You need reviewers in ${nTimeZonesRequired - actualTimeZones} more timezone(s). " +
            "Currently you have: ${timeZones.join(", ")}.")

Have questions? Visit the Atlassian Community to connect, share, and learn with other Atlassian users and experts, including Adaptavist staff.

Ask a question about ScriptRunner for JIRA, Bitbucket Server, or Confluence.

Want to learn more? Check out courses on Adaptavist Learn, an online platform to onboard and train new users for Atlassian solutions.