Getting Started

Stash Extension Points

Miscellaneous

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:

  • require a minimum number of reviewers

  • require reviewers of different levels of seniority

  • require more reviewers when the pull request author is an intern

  • require a minimum level of test coverage for newly-added or changed code

  • enforce that reviewers are in different time zones, to encourage knowledge sharing between regions

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

pathsMatch('glob:path/to/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"

Working with Custom Merge Checks

The mergeRequest is contained in the script binding.

To block the merge you should call the veto
method on it, for example, a complete script that blocks all merges is:

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

Samples

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 StashBaseScript baseScript
MergeRequest mergeRequest = mergeRequest

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

/**
 * 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)
public static List<String> getTimeZonesForUsers(ApplicationLink jiraLink, Set<String> participants) {

    def authenticatedRequestFactory = jiraLink.createAuthenticatedRequestFactory()
    def log = Logger.getLogger(this.class)
    def timeZones = participants.findResults { key ->
        authenticatedRequestFactory
            .createRequest(Request.MethodType.GET, "rest/api/2/user?username=$key")
            .addHeader("Content-Type", "application/json")
            .execute([
            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>
        )
    }
    timeZones
}

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

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