ScriptRunner for Jira Cloud Logo

Getting Started

JIRA Extension Points

Releases

ScriptRunner script listeners run a script when they are triggered by one or more webhook events. Webhooks are fired after an action takes place in Jira, for example, when an issue is updated or a project is created.

For example, you can use a script listener to populate a project with initial issues when it is created. Script listeners can also be used to send notifications outside of the standard notification scheme.

Warning
When creating listeners, be careful that you do not create an 'endless loop scenario'. For example, configuring a listener to add a comment on an event which triggers a comment related event. This can cause performance issues. To fix this, disable the problem listener.

Create a Script Listener

  1. Navigate to ScriptRunner → Script Listeners.

  2. Click Create Script Listener.

  3. Enter the name of the listener in Script Listener Called.

  4. Select the event(s) you wish the listener script to trigger on in On These Events, for example, Issue Updated.

  5. Select the projects you want the listener to be active for; you can select All Projects or a number of individual projects.

    Note
    Project settings only apply to issue, project, issuelink (source issue), version, and comment related events.
  6. Choose the user you wish to run the listener as in As This User. See As This User for more information.

  7. Enter a condition on which the code will run. See Evaluate Condition for more information.

  8. Write your script in the Code to Run field. This code is executed when the Evaluate Condition is true. Click on the examples under the code field for common use cases that can be edited to suit needs.

    Tip
    See Examples for use cases.
  9. Click Save.

Edit a Script Listener

  1. Navigate to ScriptRunner → Script Listeners. A list of all listeners is shown.

  2. Click Edit Script Listener on the listener you wish to edit.

  3. When all changes have been made, click Save.

Fields

As This User

Script listeners can make requests back to Jira using either the add-on user (ScriptRunner Add-On User) or the user that is setting up the listener (Current User).

When using the Initiating User, any action occurring as a result of the function is registered as being performed by them. For example, if an issue is commented on, the comment comes from the Initiating User rather than the obscure ScriptRunner Add-on User who may have nothing to do with the issue/project affected.

Permissions are considered when executing actions. The user selected in the Run as User field must have the correct permissions to do the action specified. Typically the ScriptRunner Add-on User has project admin permissions; however, this can be restricted. The Initiating User may have higher permissions than the ScriptRunner Add-on User.

Script listeners can make requests back to Jira using either the ScriptRunner Add-on user or the user that performed the action to cause the event to be fired. See the Run As User section of Workflow Extensions for more information.

Evaluate Condition

The condition will be evaluated before the execution of your code. In case it returns other value than true, the code will not execute. The condition is evaluated using Evaluate Jira expression, so please refer to the documentation for correct syntax and context variables available.

There is default Context variables "user" (app is not supported yet by Atlassian) and variables specific to the chosen event. e.g for issue related events the Context variables will be "issue" and "user", for sprint related events the Context variables will be "sprint" and "user"

Condition Examples

  • issue related events, "issue.comments.map(c ⇒ c.body)" stops the execution as the result is not true but a list of objects

  • issue related events, "issue.comments.length == 2" succeeds for issues with 2 comments

  • sprint related events, "issue.comments.length == 2" errors as issue is not available in the context of sprint event, code will not execute

  • issue related events, "issue.issueType.name.match('^(Bug|Task)$') != null" succeeds only for issues with issue type, Bug or Task

  • issue related events, "issue" stops the execution, you can see the returned value from the execution in the execution detail and check available fields

  • all events, "user.key == 'special-user-key'", runs the code only if the user performing the event is 'special-user-key'

Script Context

The Script Context is a set of parameters/code variables that are automatically injected into your script to provide contextual data for the script listeners. They are displayed immediately above the code editor. The parameters in the Script Context are different for each Event.

Common parameters in the Script Context for all the events are:

  • baseUrl - Base url to make API requests against. This is the URL used for relative request paths e.g. if you make a request to /rest/api/2/issue we use the baseUrl to create a full request path.

  • logger - Logger to use for debugging purposes. Check the methods available org.slf4j.Logger

  • timestamp - The timestamp of the event in milliseconds e.g. 1491562297883

  • webhookEvent - The webhook event type. Atlassian Connect Webhook Documentation

Event-specific Script Context are listed below for each event.

Projects

Script listeners are by default set to run for all projects. This can be changed per Script Listener to run for selected projects only.

This filtering only applies to issue, issuelink (source issue), version, project and comment related events. In case you require filtering on issuelinks on both source and destination issue add both projects to the filter.

Examples

Calculate Custom Field on Issue Update

Right now in Jira Cloud we can’t do a real calculated custom field, but we can update a field in response to an IssueUpdated event, creating a simple calculated custom field. The following example has 3 number custom fields configured. The code is designed to be used with the Issue Updated event

// get custom fields
def customFields = get("/rest/api/2/field")
        .asObject(List)
        .body
        .findAll { (it as Map).custom } as List<Map>

def input1CfId = customFields.find { it.name == 'Custom Field 1' }?.id
def input2CfId = customFields.find { it.name == 'Custom Field 2' }?.id
def outputCfId = customFields.find { it.name == 'Output Custom Field' }?.id
def projectKey = "TP"

if (issue == null || issue.fields.project.key != projectKey) { (1)
    logger.info("Wrong Project ${issue.fields.project.key}")
    return
}

def input1 = issue.fields[input1CfId] as Integer (2)
def input2 = issue.fields[input2CfId] as Integer

if (input1 == null || input2 == null) { (3)
    logger.info("Calculation using ${input1} and ${input2} was not possible")
    return
}

def output = input1 + input2

if (output == (issue.fields[outputCfId] as Integer)) { (4)
    logger.info("already been updated")
    return
}

put("/rest/api/2/issue/${issue.key}") (5)
    //.queryString("overrideScreenSecurity", Boolean.TRUE) (6)
    .header("Content-Type", "application/json")
    .body([
        fields:[
                (outputCfId): output
        ]
    ])
    .asString()
  1. First check to see if the issue is in the required project

  2. Grab the values of the custom fields from the issue

  3. Sanity check for null values - the issue may not have a value for any input

  4. Here we check to see if the output field already has the calculated value, if it does do nothing. This is important because an IssueUpdated event will fire for the update we are about to perform

  5. Update the value of the custom field

  6. If using the Add-on user to run the script, it is possible to set the overrideScreenSecurity property to modify fields that are not on the current screen

The example described above is for numeric fields. In case your field is of a different type or you don’t know how to retrieve the value, we recommend to use an example code bellow to retrieve the custom field from an issue and check the value. The example is retrieving value of a select field.

package com.adaptavist.sr.cloud.samples.events

def issueKey = 'EXAMPLE-1'
def customFieldName = 'customfield_10003'

def result = get("/rest/api/2/issue/${issueKey}?fields=${customFieldName}")
        .header('Content-Type', 'application/json')
        .asObject(Map)
if (result.status == 200) {
    return result.body.fields[customFieldName]
} else {
    return "Error retrieving issue ${result}"
}

// Would return
//{
//    "self": "https://example.atlassian.net/rest/api/2/customFieldOption/10000",
//    "value": "To Do",
//    "id": "10000"
//}

You can see from the example that you need to access the value of the select field via result.body.fields[customFieldName].value to be used in further computations.

Create Issues on Project Creation

When a project is created it can sometimes be desirable to create a set of setup issues to say create a confluence space or produce some project documentation. The following example creates issues on a Project Created event. It contains examples of creating individual issues and creating issues in bulk.

def projectKey = project.key as String
logger.info(projectKey)

def issueTypesReq = get("/rest/api/2/issuetype").asObject(List) (1)
assert issueTypesReq.status == 200
def taskType = issueTypesReq.body.find { it.name == "Task" }
assert taskType // Must have an issue type named Task

// create two issues

post("/rest/api/2/issue") (2)
    .header("Content-Type", "application/json")
    .body(
        [
                fields: [
                        summary    : "Create Confluence space associated to the project",
                        description: "Don't forget to do this!.",
                        project    : [
                                id: project.id
                        ],
                        issuetype  : [
                                id: taskType.id
                        ]
                ]
        ])
    .asString()

post("/rest/api/2/issue")
    .header("Content-Type", "application/json")
    .body(
        [
                fields: [
                        summary    : "Bootstrap connect add-on",
                        description: "Some other task",
                        project    : [
                                id: project.id
                        ],
                        issuetype  : [
                                id: taskType.id
                        ]
                ]
        ])
    .asString()

// example of bulk update:
post("/rest/api/2/issue/bulk")
    .header("Content-Type", "application/json")
    .body(
        [
                issueUpdates: [ (3)
                        [
                                fields: [
                                        summary    : "Bulk task one",
                                        description: "First example of a bulk update",
                                        project    : [
                                                id: project.id
                                        ],
                                        issuetype  : [
                                                id: taskType.id
                                        ]
                                ]
                        ],
                        [
                                fields: [
                                        summary    : "Bulk task two",
                                        description: "2nd example of a bulk update",
                                        project    : [
                                                id: project.id
                                        ],
                                        issuetype  : [
                                                id: taskType.id
                                        ]
                                ]
                        ]
                ]
        ])
    .asString()
  1. First get all Issue Types to find the Task type, we will use this id to create all our tasks

  2. Create a single issue of type task

  3. Note in the bulk update we use instead of using a single fields structure we nest fields inside of issueUpdates

Email Notify on Priority Change

When particular fields change it might be necessary to notify users who wouldn’t usually get notified, with some specific text to grab some attention. The following example shows how to send a notification by email to all interested parties (reporter, assignee, watchers, voters), the user admin and the jira-administrators group when the priority is changed to Highest. This functionality is useful to send messages beyond those that are usually sent by Jira.

Note: The notify API which this script uses contains a validation rule which prevents users from notifying themselves. This means that the execution will fail if the user being notified is the same user who executed the script.

import groovy.xml.MarkupBuilder
def priorityChange = changelog?.items.find { it['field'] == 'priority' }

if (!priorityChange) {
    logger.info("Priority was not updated")
    return
}
logger.info("Priority changed from {} to {}", priorityChange.fromString, priorityChange.toString)
if (priorityChange.toString == "Highest") { (1)
    def writer = new StringWriter()
    // Note that markup builder will result in static type errors as it is dynamically typed.
    // These can be safely ignored
    def markupBuilder = new MarkupBuilder(writer) (2)
    markupBuilder.div {
        p {
            // update url below:
            a(href: "http://myjira.atlassian.net/issue/${issue.key}", issue.key) (3)
            span(" has had priority changed from ${priorityChange.fromString} to ${priorityChange.toString}")
        }
        p("You're important so we thought you should know")
    }
    def htmlMessage = writer.toString()
    def textMessage = new XmlSlurper().parseText(htmlMessage).text() (4)

    logger.info("Sending email notification for issue {}", issue.key)
    def resp = post("/rest/api/2/issue/${issue.id}/notify") (5)
            .header("Content-Type", "application/json")
            .body([
            subject: 'Priority Increased',
            textBody: textMessage,
            htmlBody: htmlMessage,
            to: [
                    reporter: issue.fields.reporter != null, // bug - 500 error when no reporter
                    assignee: issue.fields.assignee != null, // bug - 500 error when no assignee
                    watchers: true,
                    voters: true,
                    users: [[
                                    name: 'admin'
                            ]],
                    groups: [[
                                     name: 'jira-administrators'
                             ]]
            ]
    ])
            .asString()
    assert resp.status == 204
}
  1. Only do anything here if the priority change was to Highest

  2. Generate a HTML message using MarkupBuilder

  3. The issue URI here is created manually. It should be updated

  4. Quick hack to produce a text only message from the HTML string

  5. Post to the Notify endpoint. Note there is a JIRA bug where a missing reporter or assignee will cause a 500 error. The example here is of sending the message to the reporter and assignee, any watchers and voters as well as a user and group list

Store Sub-task Estimates in Parent issue on Issue events

When a sub-task is created, updated or deleted you might want to update the parent issue with some information. This example stores the sum of the Estimated fields from each sub-task in their parent issue.

if (!issue.fields.issuetype.subtask) { (1)
    return
}

// Get the parent issue as a Map
def parent = (issue.fields as Map).parent as Map

// Retrieve all the subtasks of this issue's parent
def parentKey = parent.key

def allSubtasks = get("/rest/api/2/search")
        .queryString("jql", "parent=${parentKey}")
        .queryString("fields", "parent,timeestimate") (2)
        .asObject(Map)
        .body
        .issues as List<Map>
logger.info("Total subtasks for ${parentKey}: ${allSubtasks.size()}")

// Sum the estimates
def estimate = allSubtasks.collect { Map subtask ->
    subtask.fields.timeestimate ?: 0  (3)
}.sum()
logger.info("Summed estimate: ${estimate}")

// Get the field ids
def fields = get('/rest/api/2/field')
        .asObject(List)
        .body as List<Map>

def summedEstimateField = fields.find { it.name == "Summed Subtask Estimate" }.id (4)
logger.info("Custom field ID to update: ${summedEstimateField}")

// Now update the parent issue
def result = put("/rest/api/2/issue/${parentKey}")
        .header('Content-Type', 'application/json')
        .body([
            fields: [
                (summedEstimateField): estimate
            ]
        ])
        .asString()

// check that updating the parent issue worked
assert result.status >= 200 && result.status < 300
  1. Stop running the script if this issue isn’t a subtask. If you wanted to restrict the script listener to only issues in a particular project you could use: if (!(issue.fields.issuetype.subtask && issue.fields.project.key == 'EXAMPLE')) {

  2. Only retrieve the fields we’re interested in, to speed the search up

  3. Here we handle the scenario where the timeestimate is not set

  4. This line means we don’t need to put the custom field ID in our script, we can refer to it by name

Store Sub-task Story Points Summed value in Parent issue on Issue events

When a sub-task is created, updated or deleted you might want to update the parent issue with some information. This example stores the sum of the story points field from each sub-task in their parent issue.

if (!issue.fields.issuetype.subtask) { (1)
    return
}

// Get the parent issue as a Map
def parent = (issue.fields as Map).parent as Map

// Retrieve all the subtasks of this issue's parent
def parentKey = parent.key

// Get the field ids
def fields = get('/rest/api/2/field')
        .asObject(List)
        .body as List<Map>

// Get the story points custom field to use in the script
def storyPointsField =  fields.find { it.name == "Story Points" }.id
logger.info("The id of the story points field is: $storyPointsField")

// Note:  The search API is limited that to only be able to return a maximum of 50 results
def allSubtasks = get("/rest/api/2/search")
        .queryString("jql", "parent=${parentKey}")
        .queryString("fields", "parent,$storyPointsField") (2)
        .asObject(Map)
        .body
        .issues as List<Map>

logger.info("Total subtasks for ${parentKey}: ${allSubtasks.size()}")

// Sum the estimates
def estimate = allSubtasks.collect { Map subtask ->
    subtask.fields[storyPointsField] ?: 0  (3)

}.sum()
logger.info("Summed estimate: ${estimate}")

// Store the summed estimate on the Story Points field of the parent issue
def summedEstimateField = fields.find { it.name == "Story Points" }.id  (4)

logger.info("Custom field ID to update: ${summedEstimateField}")

// Now update the parent issue
def result = put("/rest/api/2/issue/${parentKey}")
        .header('Content-Type', 'application/json')
        .body([
        fields: [
                (summedEstimateField): estimate
        ]
])
        .asString()

// check that updating the parent issue worked
assert result.status >= 200 && result.status < 300
  1. Stop running the script if this issue isn’t a subtask.

  2. Here we only retrieve the story points field to make the search faster.

  3. Here we get the story points value of each sub task so that they can be summed together.

  4. This line means we don’t need to put the custom field ID in our script, we can refer to the Story Points field by name.

Store the Number of Sub-tasks in the Parent

Similar to the example above, you might want to store the number of sub-tasks in the parent issue when a sub-task is created, updated or deleted.

if (!issue.fields.issuetype.subtask) { (1)
    return
}

// Retrieve all the subtasks of this issue's parent
def parentKey = issue.fields.parent.key
def allSubtasks = get("/rest/api/2/search")
        .queryString("jql", "parent=${parentKey}") (2)
        .queryString("fields", "[]") (3)
        .asObject(Map)
        .body
        .issues as List<Map>
logger.info("Total subtasks for ${parentKey}: ${allSubtasks.size()}")

// Get the field ids
def fields = get('/rest/api/2/field')
        .asObject(List)
        .body as List<Map>

def subtaskCount = fields.find { it.name == "Subtask Count" }.id (4)
logger.info("Custom field ID to update: ${subtaskCount}")

// Now update the parent issue
def result = put("/rest/api/2/issue/${parentKey}")
        .header('Content-Type', 'application/json')
        .body([
            fields: [
                (subtaskCount): allSubtasks.size()
            ]
        ])
        .asString()

// check that updating the parent issue worked
assert result.status >= 200 && result.status < 300
  1. Stop running the script if this issue isn’t a subtask.

  2. You could extend this query string to only return unresolved subtasks, or subtasks of a particular type etc

  3. We don’t need any fields because we just want the total number of results

  4. This line means we don’t need to put the custom field ID in our script, we can refer to it by name

Add a Comment on Issue Created

If you have a support project, you might want to respond to newly created issues with a canned response. This script adds a comment to each new issue in a particular project.

def projectKey = 'SUPPORT'

// Restrict this script to only run against issues in one project
if (issue.fields.project.key != projectKey) {
    return
}

def author = issue.fields.creator.displayName

// Add a plain-text comment, see https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-addComment
// for more details
def commentResp = post("/rest/api/2/issue/${issue.key}/comment")
    .header('Content-Type', 'application/json')
    .body([
            body: """Thank you ${author} for creating a support request.

We'll respond to your query within 24hrs.

In the meantime, please read our documentation: http://example.com/documentation"""
    ])
    .asObject(Map)

assert commentResp.status == 201

Post to Slack When Issue Created

Add this listener to the Issue Created event type to post a notification to Slack when an issue is created.

// Specify the key of the issue to get the fields from
def issueKey = issue.key

// Get the issue summary
def summary = issue.fields.summary

// Get the issue description
def description = issue.fields.description

// Specify the name of the slack room to post to
def channelName = '<ChannelNameHere>'

// Specify the name of the user who will make the post
def username = '<UsernameHere>'

// Specify the message metadata
Map msg_meta = [ channel: channelName, username: username ,icon_emoji: ':rocket:']

// Specify the message body which is a simple string
Map msg_dets = [text: "A new issue was created with the details below: \n Issue key = ${issueKey} \n Issue Sumamry = ${summary} \n Issue Description = ${description}"]

// Post the constructed message to slack
def postToSlack = post('https://slack.com/api/chat.postMessage')
        .header('Content-Type', 'application/json')
        .header('Authorization', "Bearer ${SLACK_API_TOKEN}") // Store the API token as a script variable named SLACK_API_TOKEN
        .body(msg_meta + msg_dets)
        .asObject(Map)
        .body

assert postToSlack : "Failed to create Slack message check the logs tab for more details"

Additional Examples

Attachment Created

Additional Bindings available in the Script:

logger.info("Attachment: {} has been created by {}. If you have a permission to view it, you can download it via link: {}",
        attachment.filename, attachment.author.displayName, attachment.content)

Attachment Deleted

Additional parameters available from the Script Context:

logger.info("Attachment: {} has been deleted by {}.", attachment.filename, attachment.author.displayName)

Board Configuration Changed

Additional Bindings available in the Script:

  • property - The Jira configuration as a Map. Available fields: self, key, value.

logger.info("Configuration for board: {} with type: {} has been changed.", configuration.name, configuration.type)
def getConfigurationResponse = get("/rest/agile/1.0/board/${configuration.id}/configuration")
        .asObject(Map)
if (getConfigurationResponse.status == 200) {
    def newConfig = getConfigurationResponse.body
    logger.info("New configuration is: {}", newConfig)
} else {
    logger.error("Failed to get board configuration with id: {}", configuration.id)
}

Board Created

Additional Bindings available in the Script:

logger.info("Board with id: {}, name: {} and type: {} has been created.", board.id, board.name, board.type)

Board Deleted

Additional Bindings available in the Script:

logger.info("Board with id: {}, name: {} and type: {} has been deleted.", board.id, board.name, board.type)

Board Updated

Additional Bindings available in the Script:

logger.info("Board with id: {}, name: {} and type: {} has been updated.", board.id, board.name, board.type)

Comment Created

Additional Bindings available in the Script:

  • comment - The comment details as a Map. See Get Comment REST API reference for details.

  • issue - Limited issue details as a Map. It has id, self, key and fields(status, priority, assignee, project, issuetype, summary) properties.

logger.info("Comment: '{}' has been created by: {} for issue with key: {}", comment.body, comment.author.displayName, issue.key)

Comment Deleted

Additional Bindings available in the Script:

  • comment - The comment details as a Map. See Get Comment REST API reference for details.

  • issue - Limited issue details as a Map. It has id, self, key and fields(status, priority, assignee, project, issuetype, summary) properties.

logger.info("Comment: '{}' has been deleted by: {} from issue with key: {}", comment.body, comment.author.displayName, issue.key)

Comment Updated

Additional Bindings available in the Script:

  • comment - The comment details as a Map. See Get Comment REST API reference for details.

  • issue - Limited issue details as a Map. It has id, self, key and fields(status, priority, assignee, project, issuetype, summary) properties.

logger.info("Comment: '{}' has been updated by: {} in issue with key: {}", comment.body, comment.author.displayName, issue.key)

Issue Created

Additional Bindings available in the Script:

logger.info("Issue: {} has been created by: {} in project: {}", issue.key, user.displayName, issue.fields.project.key)

Issue Deleted

Additional Bindings available in the Script:

logger.info("Issue: {} has been deleted by: {} in project: {}", issue.key, user.displayName, issue.fields.project.key)

Issue Updated

Additional Bindings available in the Script:

logger.info("Issue: {} has been updated by: {} in project: {}", issue.key, user.displayName, issue.fields.project.key)

Additional Bindings available in the Script:

def sourceIssueResponse = get("/rest/api/2/issue/${issueLink.sourceIssueId}").asObject(Map)
assert sourceIssueResponse.status == 200
def sourceIssueKey = sourceIssueResponse.body.key
def sourceIssueSummary = sourceIssueResponse.body.fields.summary

def destinationIssueResponse = get("/rest/api/2/issue/${issueLink.destinationIssueId}").asObject(Map)
assert destinationIssueResponse.status == 200
def destinationIssueKey = destinationIssueResponse.body.key
def destinationIssueSummary = destinationIssueResponse.body.fields.summary

logger.info("A link has been created: Issue {} {} issue {}", sourceIssueKey, issueLink.issueLinkType.outwardName, destinationIssueKey)

Additional Bindings available in the Script:

def sourceIssueResponse = get("/rest/api/2/issue/${issueLink.sourceIssueId}").asObject(Map)
assert sourceIssueResponse.status == 200
def sourceIssueKey = sourceIssueResponse.body.key
def sourceIssueSummary = sourceIssueResponse.body.fields.summary

def destinationIssueResponse = get("/rest/api/2/issue/${issueLink.destinationIssueId}").asObject(Map)
assert destinationIssueResponse.status == 200
def destinationIssueKey = destinationIssueResponse.body.key
def destinationIssueSummary = destinationIssueResponse.body.fields.summary

logger.info("A link has been deleted: Issue {} {} issue {}", sourceIssueKey, issueLink.issueLinkType.outwardName, destinationIssueKey)

Option Attachments Changed

This event is only fired when the Allow Attachments setting on the Attachments advanced settings page in Jira has changed. Additional Bindings available in the Script:

  • property - The Jira configuration as a Map.

Example code will be delivered soon.

This event is only fired when activating or deactivating Issue linking on the Issue linking page in Jira changes. Additional Bindings available in the Script:

  • property - The Jira configuration as a Map. Available fields: self, key, value.

Example code will be delivered soon.

Option Sub-tasks Changed

Additional Bindings available in the Script:

  • property - The Jira configuration as a Map. Available fields: self, key, value.

Example code will be delivered soon.

Option Timetracking Changed

Additional Bindings available in the Script:

  • property - The JIRA configuration as a Map. Available fields: self, key, value.

logger.info("Your configuration has changed. Key: {} has a new value: {}", property.key, property.value)

Option Unassigned Issues Changed

This event is only fired when you change the Allow unassigned issues setting on the General configuration page in Jira is changed. Additional Bindings available in the Script:

  • property - The Jira configuration as a Map. Available fields: self, key, value.

Example code will be delivered soon.

Option Voting Changed

This event is only fired when you change the Voting setting on the General configuration page in Jira is changed. Additional Bindings available in the Script:

  • property - The Jira configuration as a Map. Available fields: self, key, value.

logger.info("Your configuration has changed. Key: {} has a new value: {}", property.key, property.value)

Option Watching Changed

This event is only fired when you change the Watching setting on the General configuration page in Jira is changed. Additional Bindings available in the Script:

  • property - The Jira configuration as a Map. Available fields: self, key, value.

logger.info("Your configuration has changed. Key: {} has a new value: {}", property.key, property.value)

Project Created

Additional Bindings available in the Script:

def projectKey = project.key as String

def issueTypesReq = get("/rest/api/2/issuetype").asObject(List)
assert issueTypesReq.status == 200
def taskType = issueTypesReq.body.find { it.name == "Task" }
assert taskType // Must have an issue type named Task

def response = post("/rest/api/2/issue/bulk")
        .header("Content-Type", "application/json")
        .body(
        [
                issueUpdates: [
                        [
                                fields: [
                                        summary    : "Issue summary",
                                        description: "Issue description",
                                        project    : [
                                                id: project.id
                                        ],
                                        issuetype  : [
                                                id: taskType.id
                                        ]
                                ]
                        ],
                        [
                                fields: [
                                        summary    : "Second issue summary",
                                        description: "Second issue descriptor",
                                        project    : [
                                                id: project.id
                                        ],
                                        issuetype  : [
                                                id: taskType.id
                                        ]
                                ]
                        ]
                ]
        ])
        .asString()
if (response.status == 201) {
        logger.info("Successfully added issues to project with key: {}", projectKey)
} else {
        logger.error("Failed to bulk add issues to project with key: {}", projectKey)
}

Project Deleted

When a project is deleted it will be moved to “Trash” before it will be automatically deleted after 60 days. Projects can also be permanently deleted manually before the 60-day window expires. This event is fired when a project is deleted. To handle moved to "Trash" events, use Project Soft Deleted webhooks.

Additional Bindings available in the Script:

logger.info("Project: {} that was led by {} has been deleted.", project.key, project.lead.displayName)

Project Soft Deleted

This event is only fired after a project is moved to "Trash".

Additional Bindings available in the Script:

logger.info("Project: {} that was led by {} has been deleted.", project.key, project.lead.displayName)

Project Updated

Additional Bindings available in the Script:

logger.info("Project: {} that is led by {} has been updated.", project.key, project.lead.displayName)

Sprint Closed

Additional Bindings available in the Script:

def getBoardResponse = get("/rest/agile/1.0/board/${sprint.originBoardId}").asObject(Map)
assert getBoardResponse.status == 200
def board = getBoardResponse.body

logger.info("Sprint '{}' with id: {} has been closed. Its origin board is: '{}' with type: {}", sprint.name, sprint.id, board.name, board.type)

Sprint Created

Additional Bindings available in the Script:

def issuesToAdd = ["ISSUE-1", "ISSUE-2"]
def addIssueToSprintResponse = post("/rest/agile/1.0/sprint/${sprint.id}/issue")
        .header("Content-Type", "application/json")
        .body(
        [issues: issuesToAdd
        ])
        .asString()

if (addIssueToSprintResponse.status == 204) {
    logger.info("To sprint '{}' with id: {} added issues: {}", sprint.name, sprint.id, issuesToAdd)
} else {
    logger.error("Failed to add issues: {} to sprint '{}' with id: {}", issuesToAdd, sprint.name, sprint.id)
}

Sprint Deleted

Additional Bindings available in the Script:

logger.info("Sprint '{}' with id: {} has been deleted. It was connected with board id: {}", sprint.name, sprint.id, sprint.originBoardId)

Sprint Started

Additional Bindings available in the Script:

/** you should make this request paginated if you expect more that some sprints may start with more that 100 issues
 * */
def getSprintIssuesResponse = get("/rest/agile/1.0/sprint/${sprint.id}/issue?maxResults=100").asObject(Map)
if (getSprintIssuesResponse.status != 200) {
    logger.error("Failed to get issues for script with id: {}. Service responded with code: {}", sprint.id, getSprintIssuesResponse.status)
    return
}
def sprintIssues = getSprintIssuesResponse.body

logger.info("Sprint '{}' with id: {} has been started with issues: {}", sprint.name, sprint.id, sprintIssues.issues.collect { it.key })

Sprint Updated

Additional Bindings available in the Script:

logger.info("Sprint '{}' with id: {} has been updated. Start creating new one for the same board(id: {})",
        sprint.name, sprint.id, sprint.originBoardId)
def newSprintDetails =
        [
                name: "sprint 1",
                startDate: "2020-04-11T15:22:00.000+10:00",
                endDate: "2022-04-20T01:22:00.000+10:00",
                originBoardId: sprint.originBoardId,
                goal: "sprint 1 goal"
        ]

def createNewSprintRequest = post("/rest/agile/1.0/sprint")
        .header("Content-Type", "application/json")
        .body(newSprintDetails)
        .asString()

if (createNewSprintRequest.status == 201) {
    logger.info("Successfully created a new sprint with details: {}", createNewSprintRequest.body)
} else {
    logger.error("Failed to create a new sprint with details: {}", newSprintDetails)
}

User Created

Additional Bindings available in the Script:

def projectKey = "YOUR-PROJECT-KEY"

def issueTypesReq = get("/rest/api/2/issuetype").asObject(List)
assert issueTypesReq.status == 200
def taskType = issueTypesReq.body.find { it.name == "Task" }
assert taskType // Must have an issue type named Task

def response = post("/rest/api/2/issue/bulk")
        .header("Content-Type", "application/json")
        .body(
        [
                issueUpdates: [
                        [
                                fields: [
                                        summary    : "Prepare the welcome pack for " + user.displayName,
                                        description: "Please resolve this issue once it's been picked up.",
                                        project    : [
                                                key: projectKey
                                        ],
                                        issuetype  : [
                                                id: taskType.id
                                        ]
                                ]
                        ],
                        [
                                fields: [
                                        summary    : "Please pick up the welcome pack",
                                        description: "It should be available to pick up from the reception desk 3 days after this ticket was created.",
                                        project    : [
                                                key: projectKey
                                        ],
                                        issuetype  : [
                                                id: taskType.id
                                        ],
                                        assignee: [
                                                name: user.accountId]
                                ]
                        ]
                ]
        ])
        .asObject(Map)

if (!response.status == 201) {
    logger.error("Failed to create initial tickets for user: {} in project: {}", user.displayName, projectKey)
    return
}
logger.info("Successfully created initial tickets: {} for user: {} in project: {}", response.body.issues.collect { it.key }, user.displayName, projectKey)

User Deleted

Additional Bindings available in the Script:

logger.info('User with name: {} and accountId: {} has been deleted.', user.displayName, user.accountId)

User Updated

Additional Bindings available in the Script:

logger.info('User with displayName: {} and accountId: {} has been updated.', user.displayName, user.accountId)

Version Created

Additional Bindings available in the Script:

def getProjectDetailsRequest = get("/rest/api/2/project/${version.projectId}").asObject(Map)
if (getProjectDetailsRequest.status == 200) {
    def projectDetails = getProjectDetailsRequest.body
    logger.info("Version with name: {} has been created for project: {}", version.name, projectDetails.name)
} else {
    logger.error("Failed to get project details for freshly created version with name: {}", version.name)
}

Version Deleted

Additional Bindings available in the Script:

logger.info("Version: [name: {}, self: {}, id: {}, description: {}, archived: {}, released: {}, overdue: {}, userReleaseDate: {}, projectId: {}] has been deleted.",
        version.name, version.self, version.id, version.description, version.archived, version.released, version.overdue, version.userReleaseDate, version.projectId)

Version Moved

Additional Bindings available in the Script:

logger.info("Version: [name: {}, self: {}, id: {}, description: {}, archived: {}, released: {}, overdue: {}, userReleaseDate: {}, projectId: {}] has been moved.",
        version.name, version.self, version.id, version.description, version.archived, version.released, version.overdue, version.userReleaseDate, version.projectId)

Version Released

Additional Bindings available in the Script:

logger.info("Version: {} has been released", version.name)
def newVersionName = version.name + "_bugfix"
def createRCVersionRequest = post("/rest/api/2/version")
        .header("Content-Type", "application/json")
        .body(
        [
                description: version.description,
                name: newVersionName,
                archived: version.archived,
                released: version.released,
                userReleaseDate: version.userReleaseDate,
                projectId: version.projectId
        ])
        .asString()
if (createRCVersionRequest.status == 201) {
    logger.info("Successfully create new version with name: {}", newVersionName)
} else {
    logger.error("Failed to create new version with name: {}", newVersionName)
}

Version Unreleased

Additional Bindings available in the Script:

logger.info("Version: [name: {}, self: {}, id: {}, description: {}, archived: {}, released: {}, overdue: {}, userReleaseDate: {}, projectId: {}] has not been released.",
        version.name, version.self, version.id, version.description, version.archived, version.released, version.overdue, version.userReleaseDate, version.projectId)

Version Updated

Additional Bindings available in the Script:

logger.info("Version with name: {} has been updated", version.name)
def unresolvedIssueCountRequest = get("/rest/api/2/version/${version.id}/unresolvedIssueCount").asObject(Map)
if (unresolvedIssueCountRequest.status == 200) {
    def unresolvedIssueBody = unresolvedIssueCountRequest.body
    logger.info("There are unresolved issues: {}/{} for version with name: {}", unresolvedIssueBody.issuesUnresolvedCount, unresolvedIssueBody.issuesCount, version.name)
} else {
    logger.error("Failed to get unresolver issue count for version with name: {}", version.name)
}

Worklog Created

Additional Bindings available in the Script:

def worklogPropertiesRequest = get("/rest/api/2/issue/${worklog.issueId}/worklog/${worklog.id}/properties").asObject(Map)
if (worklogPropertiesRequest.status == 200) {
    logger.info("Worklog with id: {} has been created for issueId: {} by author: {} with properties: {}", worklog.id, worklog.issueId, worklog.author.displayName, worklogPropertiesRequest.body)
} else {
    logger.error("Failed to get properties for worklog with id: {}", worklog.id)
}

Worklog Deleted

Additional Bindings available in the Script:

logger.info("Worklog with id: {} that was initially created by: {} has been deleted by {}", worklog.id, worklog.author.displayName, worklog.updateAuthor.displayName)

Worklog Updated

Additional Bindings available in the Script:

logger.info("Worklog with self link: {} has been updated by {}", worklog.self, worklog.updateAuthor.displayName)