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 listener 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 // <1>
def projectKey
if (event instanceof ProjectCreationRequestedEvent) {
projectKey = event.getProject().getKey()
} else if (event instanceof ProjectModificationRequestedEvent) {
projectKey = event.getNewValue().getKey()
}
<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 → Listeners. 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 // <1>
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")
}
<1>: imports will generally not be displayed, for clarity
This script would be attached to the ProjectCreationRequestedEvent
event.
Applying listeners 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-cancelable 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" // <1>
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()
}
<1>: 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.bitbucket.integration.jira.JiraIssueService
import com.atlassian.sal.api.UrlMode
import com.atlassian.sal.api.net.Request
import com.atlassian.sal.api.net.Response
import com.onresolve.scriptrunner.canned.bitbucket.util.BitbucketBaseScript
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
@BaseScript BitbucketBaseScript baseScript
@PluginModule
JiraIssueService jiraIssueService
def pullRequest = event.getPullRequest()
def repository = pullRequest.toRef.repository
assert pullRequest
def keys = jiraIssueService.getIssuesForPullRequest(repository.id, pullRequest.id)*.key // <1>
def jiraLink = getJiraAppLink()
def authenticatedRequestFactory = jiraLink.createImpersonatingAuthenticatedRequestFactory() ?:
jiraLink.createAuthenticatedRequestFactory()
// 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 -> // <2>
authenticatedRequestFactory // <3>
.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>
)
}
<1>: get all issue keys related to this PR
<2>: iterate over the issues
<3>: 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") // <1>
.addHeader("Content-Type", "application/json")
.execute([
handle: { Response response ->
new JsonSlurper().parse(response.responseBodyAsStream)
}
] as ApplicationLinkResponseHandler<String>
)
if (!(jiraVersions.any { !it.released })) { // <2>
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))
}
<1>: Get list of versions. Note: project SD is hard-coded here
<2>: 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 default branch on repository creation: https://answers.atlassian.com/questions/44219697/answers/44224550