Custom Listeners

Working with Custom Listeners

The event is contained in the script binding. For cancelable events this will implement com.atlassian.bitbucket.event.CancelableEvent.

If it is cancelable, you can prevent the operation proceeding by calling cancel on it.

There is a shorter version supplied if you do not want to worry about supplying a translated message to the user:cancel(String message).

event.cancel('foo')

You may choose to have your handler listen for multiple different events. If you need to do different things depending on the type of event, you can check that with instanceof.

For example, in a listener that handles project modification and project creation, getting the project key depends on whether the project is being updated or modified:

ApplicationEvent event = event def projectKey if (event instanceof ProjectCreationRequestedEvent) { projectKey = event.getProject().getKey() } else if (event instanceof ProjectModificationRequestedEvent) { projectKey = event.getNewValue().getKey() }

Line 1: event is passed in the binding - this line is only used to give type information when using an IDE, and has no functional impact

Samples

Controlling personal repository creation

To use, go to Admin → Script Event Handlers. In the Events field, choose either or both of RepositoryCreationRequestedEvent and RepositoryForkRequestedEvent. One or the other (not both) will be fired depending on whether it’s a new personal repo, or a fork into the user’s personal project.

Allow only 5 personal repos

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.bitbucket.project.ProjectType import com.atlassian.bitbucket.repository.RepositoryService // only allow N personal def repositoryService = ComponentLocator.getComponent(RepositoryService) final Integer MAX_PERSONAL = 5 def event = event as RepositoryCreationRequestedEvent def project = event.repository.project if (project.type == ProjectType.PERSONAL) { if (repositoryService.countByProject(project) >= MAX_PERSONAL) { event.cancel("You can only create $MAX_PERSONAL personal repositories.") } }

User in named directory

Allow personal repo creation only if the user is in a certain named directory:

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent import com.atlassian.crowd.embedded.api.CrowdDirectoryService import com.atlassian.crowd.embedded.api.CrowdService import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.bitbucket.project.ProjectType def crowdService = ComponentLocator.getComponent(CrowdService) def crowdDirectoryService = ComponentLocator.getComponent(CrowdDirectoryService) def event = event as RepositoryCreationRequestedEvent def project = event.repository.project // only allow when user comes from dir X if (project.type == ProjectType.PERSONAL && event.user) { def cwdUser = crowdService.getUser(event.user.name) def directory = crowdDirectoryService.findDirectoryById(cwdUser.directoryId) if (!(directory.name in ["Bitbucket Internal Directory", "Stash Internal Directory"])) { // enter directory name event.cancel("You don't have permissions to create a personal repository.") } }

Allow 1 Gb of personal repository space

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.bitbucket.project.ProjectType import com.atlassian.bitbucket.repository.Repository import com.atlassian.bitbucket.repository.RepositoryService import com.atlassian.bitbucket.util.Page import com.atlassian.bitbucket.util.PageProvider import com.atlassian.bitbucket.util.PageRequest import com.atlassian.bitbucket.util.PagedIterable // only allow N personal def repositoryService = ComponentLocator.getComponent(RepositoryService) def event = event as RepositoryCreationRequestedEvent def project = event.repository.project // allow only 1Gb total size per user if (project.type == ProjectType.PERSONAL) { def totalUserSize = new PagedIterable<Repository>(new PageProvider<Repository>() { @Override Page<Repository> get(PageRequest pageRequest) { repositoryService.findByProjectKey(project.key, pageRequest) as Page<Repository> } }, 100).sum { Repository repo -> repositoryService.getSize(repo) } if (totalUserSize >= 1024e3) { event.cancel("You can only use 1Gb in personal repository space, which you have exceeded.") } }

Only allow from groups

Allow only members of certain groups to create personal repos:

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.bitbucket.project.ProjectType import com.atlassian.bitbucket.user.UserService def userService = ComponentLocator.getComponent(UserService) def event = event as RepositoryCreationRequestedEvent def project = event.repository.project if (project.type == ProjectType.PERSONAL) { if (!userService.isUserInGroup(event.getUser(), "personal-project-creators")) { event.cancel("You don't have permissions to create a personal repository.") } }

Disallowing project creation except for members of a group

import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.bitbucket.user.UserService def userService = ComponentLocator.getComponent(UserService) if (!userService.isUserInGroup(event.getUser(), "project-creators")) { event.cancel("Only users in the project-creators group can create projects") }


Line 1: imports will generally not be displayed, for clarity

This script would be attached to the ProjectCreationRequestedEvent event.

Applying event handlers to specific projects/repositories

You may find in a custom listener that you need to apply it only to particular projects or repositories.

For example to apply the allow only 5 personal repos custom listener to a specific project you can use the example below.

package examples.bitbucket.handler import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent import com.atlassian.bitbucket.repository.RepositoryService import com.atlassian.sal.api.component.ComponentLocator def repositoryService = ComponentLocator.getComponent(RepositoryService) final Integer MAX_PER_TEST = 5 def event = event as RepositoryCreationRequestedEvent def project = event.repository.project if (project.key == "TEST") { if (repositoryService.countByProject(project) >= MAX_PER_TEST) { event.cancel("You can only create $MAX_PER_TEST repositories in test projects.") } }

You could also apply it to a particular set of projects:

if (project.key in ["PTEST", "PTEST1", "PTEST2"]) { ... }

If you want to apply the listener to specific repositories instead you can use the following example below:

if (event.repository.project.key == "TEST" && event.repository.slug in [ "one", "two", "three" ]) { ... }

If applying to specific repositories you need to check both the project key and repository slug as the repository slug is only unique to a project.

Post to Slack when a repository is forked

The same principle can be used to integrate with anything that provides a web service, as an example we use Slack.

Typically you would add this code to a non-canceleable event, so that the other system gets notified only if the operation is successful.

package examples.bitbucket.handler import com.atlassian.bitbucket.event.repository.RepositoryForkedEvent import groovy.json.JsonBuilder import groovyx.net.http.ContentType import groovyx.net.http.HTTPBuilder import groovyx.net.http.Method RepositoryForkedEvent event = event def http = new HTTPBuilder("https://hooks.slack.com") http.request(Method.POST, ContentType.TEXT) { uri.path = "/services/XXXX/YYYYY/ZZZZZ" body = new JsonBuilder([ channel : "#dev", username : "webhookbot", text : "New fork created on repo: *${event.repository.name}* " + "by *${event.user.displayName}*. Come and join our chat!", icon_emoji: ":ghost:", mrkdwn : true, ]).toString() }

Line 13: Slack will give you this value

This script would be attached to the RepositoryForkedEvent event.

Update all related Jira issues when Pull Request Opened

This sample will update linked issues on the creation of a Pull Request. This may be useful for informing stakeholders on progress.

package examples.bitbucket.handler import com.atlassian.applinks.api.ApplicationLinkResponseHandler import com.atlassian.plugin.PluginAccessor import com.atlassian.sal.api.UrlMode import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.sal.api.net.Request import com.atlassian.sal.api.net.Response import com.atlassian.bitbucket.event.pull.PullRequestOpenedEvent import com.atlassian.bitbucket.integration.jira.JiraIssueService import com.onresolve.scriptrunner.canned.bitbucket.util.BitbucketBaseScript import com.onresolve.scriptrunner.runner.ScriptRunnerImpl import groovy.json.JsonBuilder import groovy.transform.BaseScript @BaseScript BitbucketBaseScript baseScript PullRequestOpenedEvent event = event def pluginAccessor = ComponentLocator.getComponent(PluginAccessor) def jiraIssueService = ScriptRunnerImpl.getOsgiService(JiraIssueService) def pullRequest = event.getPullRequest() def repository = pullRequest.fromRef.repository assert pullRequest def keys = jiraIssueService.getIssuesForPullRequest(repository.id, pullRequest.id)*.key def jiraLink = getJiraAppLink() def authenticatedRequestFactory = jiraLink.createImpersonatingAuthenticatedRequestFactory() // work out the URL to the pull request def prUrl = "${getBaseUrl(UrlMode.ABSOLUTE)}/projects/" + "${repository.project.key}/repos/${repository.slug}/" + "pull-requests/${pullRequest.id}/overview" def input = new JsonBuilder([ body: "Pull Request [${pullRequest.id}|$prUrl] has been updated:\n" + "\n" + "{{${pullRequest.title}}}" ]).toString() keys.each { String key -> authenticatedRequestFactory .createRequest(Request.MethodType.POST, "rest/api/2/issue/$key/comment?expand=renderedBody") .addHeader("Content-Type", "application/json") .setEntity(input) .execute([ handle: { Response response -> if (response.successful) { log.debug "Created comment on issue: $key." } else { log.warn "Failed to create comment: $response.responseBodyAsStream" } }] as ApplicationLinkResponseHandler<Void> ) }

Line 27: get all issue keys related to this PR

Line 43: iterate over the issues

Line 44: create a comment on each issue using the Jira REST API

For this to work you must have a working application link to Jira set up.

Block Forking Unless Unreleased Versions

Another contrived example that will block repository forking unless an unreleased Jira version exists in the project. This script should be attached to the RepositoryForkRequestedEvent event.

@BaseScript BitbucketBaseScript baseScript RepositoryForkRequestedEvent event = event def jiraLink = getJiraAppLink() def jiraVersions = jiraLink.createImpersonatingAuthenticatedRequestFactory() .createRequest(Request.MethodType.GET, "rest/api/2/project/SD/versions") .addHeader("Content-Type", "application/json") .execute([ handle: { Response response -> new JsonSlurper().parse(response.responseBodyAsStream) } ] as ApplicationLinkResponseHandler<String> ) if (!(jiraVersions.any { !it.released })) { def msg = "All JIRA versions are released for this project... " + "please create a new version before forking. Current versions are: ${jiraVersions*.name.join(",")}" event.cancel(new KeyedMessage("some.key", msg, msg)) }

Line 6: Get list of versions. Note: project SD is hard-coded here

Line 15: Look for unreleased versions

Further Examples

On this page