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:
is passed in the binding - this line is only used to give type information when using an IDE, and has no functional impactevent
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
Blocking write access to users other than the owner, for personal repositories: https://answers.atlassian.com/questions/38753103/answers/38753610
Creating a master branch on repository creation: https://answers.atlassian.com/questions/44219697/answers/44224550