Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

My custom scripts for Scriptrunner and beyond

Volodymyr December 13, 2023

I'm just a guy who started my journey as a JIRA admin almost 3 years ago, and halfway through I wanted to learn how to use JIRA API for Scriptrunner, so I've written a lot of custom scripts in the last year and a half, and I'd like to share those scripts with the Community.
I want to emphasize that I'm not a developer, and I've been learning everything by myself, so my scripts can be improved and generally written differently, so feel free to add your suggestions or improvements under a script.
To avoid stretching the post into dozens of screens, scripts will be added as comments so that further discussion can continue under a specific script.
Tip for beginners:
"I have a very hard time understanding new things and concepts, so if it worked for me, it surely will work for you too".

43 comments

Comment

Log in or Sign up to comment
Volodymyr December 13, 2023

Delete Inactive Items in JIRA

The following information was taken from an article created by Jira developers:

https://confluence.atlassian.com/clean/advanced-cleanup-1018789335.html

Not my scripts, just an added explanation of why we do things this way.

1. Remove unused Workflow Schemes:

/*
We need WorkflowSchemeManager to go through each scheme and check if it is assigned to the project.
If not, add its name to the StringBuffer and delete it, so that we get a list of only deleted schemes.
    Why use StringBuffer?
A thread-safe, mutable sequence of characters. A string buffer is like a String but can be modified.
At any point in time, it contains some particular sequence of characters,
but the length and content of the sequence can be changed through certain method calls.
*/

import com.atlassian.jira.component.ComponentAccessor

def workflowSchemeManager = ComponentAccessor.getWorkflowSchemeManager()

def sb = new StringBuffer()
sb.append('Deleted inactive workflow schemes:\n')

workflowSchemeManager.schemeObjects.each {
    try {
        if (workflowSchemeManager.getProjectsUsing(workflowSchemeManager.getWorkflowSchemeObj(it.id)).size() == 0) {
            sb.append(it.name + '\n')
            workflowSchemeManager.deleteScheme(it.id)
        }
    } catch(Exception e) {
        sb.append('Error: ' + e + '\n')
    }
}

return '<pre>' + sb.toString() + '<pre>'

2. Remove unused Workflows:

/*
We need WorkflowManager to go through each workflow and check if it is assigned to the workflow scheme.
If not, add its name to the StringBuffer and delete it, so that we get a list of only deleted workflows.
    Why use StringBuffer?
A thread-safe, mutable sequence of characters. A string buffer is like a String but can be modified.
At any point in time, it contains some particular sequence of characters,
but the length and content of the sequence can be changed through certain method calls.
*/

import com.atlassian.jira.component.ComponentAccessor

def workflowManager = ComponentAccessor.getWorkflowManager()
def workflowSchemeManager = ComponentAccessor.getWorkflowSchemeManager()

def sb = new StringBuffer()
sb.append('Deleted inactive workflows:\n')

workflowManager.workflows.each {
    def workflowScheme = workflowSchemeManager.getSchemesForWorkflow(it)
    if (!it.systemWorkflow) {
        if (workflowScheme.size() == 0) {
            sb.append(it.name + '\n')
            workflowManager.deleteWorkflow(it)
        }
    }
}

return '<pre>' + sb.toString() + '<pre>'

// Please use the following script (copy of the previous one)
// if you need to delete only automatically created workflows
// that contain the phrase "Simple Issue Tracking Workflow"

import com.atlassian.jira.component.ComponentAccessor

def workflowManager = ComponentAccessor.getWorkflowManager()
def workflowSchemeManager = ComponentAccessor.getWorkflowSchemeManager()
def sb = new StringBuffer()
sb.append('Deleted inactive workflows:\n')

workflowManager.workflows.each {
    def workflowScheme = workflowSchemeManager.getSchemesForWorkflow(it)
    def workflowName = it.name

    if (!it.systemWorkflow) {
        if (workflowScheme.size() == 0 && workflowName.contains(': Simple Issue Tracking Workflow')) {
            sb.append(it.name + '\n')
            workflowManager.deleteWorkflow(it)
        }
    }
}

return '<pre>' + sb.toString() + '<pre>'

3. Remove unused Issue Type Schemes:

/*
We need IssueTypeSchemeManager to go through each scheme and check if it is assigned to the project.
If not, add its name to the StringBuffer and delete it, so that we get a list of only deleted issue type schemes.
    Why use StringBuffer?
A thread-safe, mutable sequence of characters. A string buffer is like a String but can be modified.
At any point in time, it contains some particular sequence of characters,
but the length and content of the sequence can be changed through certain method calls.
*/

import com.atlassian.jira.component.ComponentAccessor

def issueTypeSchemeManager = ComponentAccessor.getIssueTypeSchemeManager()

def sb = new StringBuffer()
sb.append('Deleted inactive issue type schemes:\n')

issueTypeSchemeManager.allSchemes.each {
    if (!it.name.contains('Default Issue Type Scheme')) {
        if (it.associatedProjectObjects.size() == 0) {
            sb.append(it.name + '\n')
            issueTypeSchemeManager.deleteScheme(it)
        }
    }
}

return '<pre>' + sb.toString() + '<pre>'

4. Remove unused Issue Type Screen Schemes:

/*
We need issueTypeScreenSchemeManager to go through each scheme and check if it is assigned to the project.
If not, add its name to the StringBuffer, remove any associations, and delete it,
so that we get a list of only deleted issue type screen schemes.
    Why use StringBuffer?
A thread-safe, mutable sequence of characters. A string buffer is like a String but can be modified.
At any point in time, it contains some particular sequence of characters,
but the length and content of the sequence can be changed through certain method calls.
*/

import com.atlassian.jira.component.ComponentAccessor

def issueTypeScreenSchemeManager = ComponentAccessor.getIssueTypeScreenSchemeManager()
def defaultIssueTypeScreenScheme = issueTypeScreenSchemeManager.defaultScheme

def sb = new StringBuffer()
sb.append('Deleted inactive issue type screen schemes:\n')

issueTypeScreenSchemeManager.issueTypeScreenSchemes.each {
    try {
        if (it == defaultIssueTypeScreenScheme) {
            // Do not delete the default scheme
            return
        }

        if (it.projects.size() == 0) {
            sb.append(it.name + '\n')
            // Remove any associations with screen schemes
            issueTypeScreenSchemeManager.removeIssueTypeSchemeEntities(it)

            // Remove the issue type screen scheme
            issueTypeScreenSchemeManager.removeIssueTypeScreenScheme(it)
        }
    } catch (Exception e) {
        sb.append('Error: ' + e + '\n')
    }
}

return '<pre>' + sb.toString() + '<pre>'

 5. Remove unused Screen Schemes:

/*
We need fieldScreenSchemeManager to go through each screen scheme and check
if it is assigned to the issue type screen scheme. If not, add its name to the StringBuffer,
remove any associations, and delete it, so that we get a list of only deleted screen schemes.
    Why use StringBuffer?
A thread-safe, mutable sequence of characters. A string buffer is like a String but can be modified.
At any point in time, it contains some particular sequence of characters,
but the length and content of the sequence can be changed through certain method calls.
*/

import com.atlassian.jira.component.ComponentAccessor

def fieldScreenSchemeManager = ComponentAccessor.getFieldScreenSchemeManager()
def issueTypeScreenSchemeManager = ComponentAccessor.getIssueTypeScreenSchemeManager()

def sb = new StringBuffer()
sb.append('Deleted inactive screen schemes:\n')

fieldScreenSchemeManager.getFieldScreenSchemes().each { fieldScreenScheme ->
    try {
        def issueTypeScreenSchemeCollection = issueTypeScreenSchemeManager.getIssueTypeScreenSchemes(fieldScreenScheme)

        // Find field screen schemes that are still associated with deleted issue type screen schemes
        def allDeleted = true
        issueTypeScreenSchemeCollection.each { issueTypeScreenScheme ->
            if (issueTypeScreenScheme != null) {
                allDeleted = false
                return
            }
        }

        // Remove field screen schemes with no (valid) associated issue type screen schemes
        if (issueTypeScreenSchemeCollection.size() == 0 || allDeleted == true) {
            sb.append(fieldScreenScheme.name + '\n')
            // Remove association to any screens
            fieldScreenSchemeManager.removeFieldSchemeItems(fieldScreenScheme)
            // Remove field screen scheme
            fieldScreenSchemeManager.removeFieldScreenScheme(fieldScreenScheme)
        }
    } catch(Exception e) {
        sb.append('Error: ' + e + '\n')
    }
}

return '<pre>' + sb.toString() + '<pre>'

6. Remove unused Screens:

/*
We need fieldScreenManager to go through each screen and check if it is assigned to the screen scheme or workflow.
If not, add its name to the StringBuffer and delete it, so that we get a list of only deleted screens.
    Why use StringBuffer?
A thread-safe, mutable sequence of characters. A string buffer is like a String but can be modified.
At any point in time, it contains some particular sequence of characters,
but the length and content of the sequence can be changed through certain method calls.
*/

import com.atlassian.webresource.api.assembler.PageBuilderService
import com.atlassian.jira.bc.issue.fields.screen.FieldScreenService
import com.atlassian.jira.issue.fields.screen.FieldScreenFactory
import com.atlassian.jira.web.action.admin.issuefields.screens.ViewFieldScreens
import com.atlassian.jira.component.ComponentAccessor

def fieldScreenManager = ComponentAccessor.getFieldScreenManager()
def fieldScreenFactory = ComponentAccessor.getComponent(FieldScreenFactory.class)
def fieldScreenSchemeManager = ComponentAccessor.getFieldScreenSchemeManager()
def fieldScreenService = ComponentAccessor.getComponent(FieldScreenService.class)
def workflowManager = ComponentAccessor.getWorkflowManager()
def jiraAuthenticationContext = ComponentAccessor.getJiraAuthenticationContext()
def pageBuilderService = ComponentAccessor.getComponent(PageBuilderService.class)

// With the help of this class, you can find out if the screen is in a screen scheme or a workflow
def viewFieldScreens = new ViewFieldScreens(fieldScreenManager,
                                            fieldScreenFactory,
                                            fieldScreenSchemeManager,
                                            fieldScreenService,
                                            workflowManager,
                                            jiraAuthenticationContext,
                                            pageBuilderService)


def sb = new StringBuffer()
sb.append('Deleted inactive screens:\n')

fieldScreenManager.getFieldScreens().each { fieldScreen ->
    // Find all screens with no (or only null/previously deleted) screen schemes or workflows
    // If the screen is not in the screen scheme or workflow, add its name to sb and delete it
    def allEmptyOrNull = true

    viewFieldScreens.getFieldScreenSchemes(fieldScreen).each { fieldScreenScheme ->
        if (fieldScreenScheme != null) {
            allEmptyOrNull = false
            return
        }
    }

    if (!allEmptyOrNull) {
        return
    }

    viewFieldScreens.getWorkflows(fieldScreen).each { workflow ->
        if (workflow != null) {
            allEmptyOrNull = false
            return
        }
    }

    if (allEmptyOrNull) {
        sb.append(fieldScreen.name + '\n')
        fieldScreenManager.removeFieldScreen(fieldScreen.getId())
    }
}

return '<pre>' + sb.toString() + '<pre>'
Volodymyr December 13, 2023

How to display a template for the "Description" field when creating an issue (Behaviours)

Add the script to Initialiser. Do not add it to the field. If you do so, the changes in the template are not saved.

import org.apache.log4j.Level
import org.apache.log4j.Logger
import com.onresolve.jira.groovy.user.FieldBehaviours
import groovy.transform.BaseScript

// Template should be displayed only when creating an issue, not when it's already created
if (underlyingIssue) {
    return
}

@BaseScript FieldBehaviours fieldBehaviours
def log = Logger.getLogger(getClass())
log.setLevel(Level.ERROR)

// Triple-single-quoted strings may span multiple lines. The content of the string can cross line boundaries
// without the need to split the string in several pieces and without concatenation or newline escape characters.
String templateStoryTask = '''
    h2. Context (mandatory)

    _Where we are at the moment of starting the story implementation. What is business/technical background? What are given preconditions? What are our motivations and assumptions?_
    2. Goal (mandatory)

    _This section should describe what we want to achieve by using feature. It should help to answer the question "why" this functionality is requested and help understand the impact vs cost of the implementation._

    h2. Acceptance criteria (mandatory)
    _Make sure that:_
    * _Acceptance criteria are testable._
    * _Criteria are clear and concise._
    * _Everyone understands your acceptance criteria._
    * _Acceptance criteria should provide user perspective._

    _Create links on KB (TAD domain, EPM domain, etc) to avoid overcomplicated formulas, bulleted lists, big cases. Put all these materials into KB and give direct anchor links._ 
    h2. Dependencies & Limitations

    _What we depend on, what blocks us from making this story feasible. Lack of knowledge, human/technical resource, decisions or agreements needed, artifacts to be prepared in advance, external teams have to do something?_ _If there are ticket dependencies (some other story to be done first or an external change request to be complete first - we use *"depends on"* relation between tickets)._
    h2. Out of scope

    _What we DO NOT do in this story. For instance, details that will be implemented in the further stories, or items that are out of our responsibility, or some strict limitations from the business point of view._
    h2. Relevant resources & Notes

    _Knowledge materials, design artifacts, best practices, spikes outcomes, product documentation, or people who can help us answer questions during our work on this story._

     _Highlighted via linked Confluence pages (Jira "link" feature for tickets). For non-confluence related materials - to be mentioned in-text._

'''.replaceAll(/    /, '')

// replaceAll() method replaces all occurrences of a captured group by the result of a closure on that text.
// In our case, before each line there is 4 backspaces for code readability,
// so we end up removing them so that the template is displayed without them.
// I've tested without this method and it worked the same way.

getFieldById('description').setFormValue(templateStoryTask)

UPDATED:
Add for ALL type of issues and use the switch to add descriptions for only specific issue types. In this case, when you change the issue type on the Create Issue screen, the field does not retain the value. For example, there is a template for a Bug, and after changing the issue type to another one for which there is no template, the previous one is saved, so you need to clear it.

import org.apache.log4j.Level
import org.apache.log4j.Logger
import com.onresolve.jira.groovy.user.FieldBehaviours
import groovy.transform.BaseScript

if (underlyingIssue) {
return
}

@BaseScript FieldBehaviours fieldBehaviours
def log = Logger.getLogger(getClass())
log.setLevel(Level.ERROR)

def description = getFieldById('description')
def issueTypeId = getIssueContext().getIssueTypeId()

String templateForBugAndSubBug = '''*Precondition:*

*Steps:*

*Actual result:*

*Expected result:* '''.replaceAll(/ /, '')

switch (issueTypeId) {
// Bug
case '1':
description.setFormValue(templateForBugAndSubBug)
break
// Sub-bug
case '78':
description.setFormValue(templateForBugAndSubBug)
break
default:
description.setFormValue('')
break

 

Volodymyr December 13, 2023

How to display different values of a customfield based on the issue status (Behaviors)

/*
The "CR status" field is of list type, where many values have been added.
We need the values in this field to be displayed differently depending on the current status of the issue.
We pass the values that should be displayed. The values are case-sensitive,
so we need to pass them exactly as they exist in the context of the field.
If there is a difference of one character, there is no such value for the field, so it will not display it.
*/

if (!underlyingIssue) {
    return
}

// Current status of the issue
def currentStatus = underlyingIssue.getStatusId()

// Customfield of type select list(single choice)
def cfCRstatus = getFieldById('customfield_34803')

// List of allowed options
List<String> allowedOptions = null

// For a particular status, the list of "CR status" field values should be different
switch(currentStatus) {
    // Open
    case '1':
        allowedOptions = []
        break
    // Ready For Implementation
    case '22136':
        allowedOptions = ['cr-status:ddo-approval']
        break
    // In progress
    case '3':
        allowedOptions = ['cr-status:ddo-approval',
                          'cr-status:system-approval',
                          'cr-status:impact-analysis',
                          'cr-status:wait-for-deploy-int',
                          'cr-status:deployed-int',
                          'cr-status:testing-int',
                          'cr-status:testing-int-eco',
                          'cr-status:notification',
                          'cr-status:wait-for-deploy-prod']
        break
    // Resolved
    case '5':
        allowedOptions = ['cr-status:wait-for-deploy-prod',
                          'cr-status:deployed-prod',
                          'cr-status:testing-prod']
        break
    // On Hold
    case '10000':
        allowedOptions = ['cr-status:ddo-approval',
                          'cr-status:system-approval',
                          'cr-status:impact-analysis',
                          'cr-status:wait-for-deploy-int',
                          'cr-status:deployed-int',
                          'cr-status:testing-int',
                          'cr-status:testing-int-eco',
                          'cr-status:notification',
                          'cr-status:wait-for-deploy-prod']
        break
    // Blocked
    case '10079':
        allowedOptions = ['cr-status:ddo-approval',
                          'cr-status:system-approval',
                          'cr-status:impact-analysis',
                          'cr-status:wait-for-deploy-int',
                          'cr-status:deployed-int',
                          'cr-status:testing-int',
                          'cr-status:testing-int-eco',
                          'cr-status:notification',
                          'cr-status:wait-for-deploy-prod',
                          'cr-status:deployed-prod',
                          'cr-status:testing-prod']
        break
    // Reopened
    case '4':
        allowedOptions = ['cr-status:ddo-approval',
                          'cr-status:system-approval',
                          'cr-status:impact-analysis',
                          'cr-status:wait-for-deploy-int',
                          'cr-status:deployed-int',
                          'cr-status:testing-int',
                          'cr-status:testing-int-eco',
                          'cr-status:notification',
                          'cr-status:wait-for-deploy-prod']
        break
    // Rejected
    case '10188':
        allowedOptions = []
        break
    // Closed
    case '6':
        allowedOptions = ['cr-status:closed']
        break
}

if (cfCRstatus) {
    cfCRstatus.setFieldOptions(allowedOptions)
}


NEW VERSION 

import com.atlassian.jira.component.ComponentAccessor

if (!underlyingIssue) {
    return
}

//Current status of the issue
def currentStatus = underlyingIssue.getStatusId()

//Customfield of type select list(single choice), its config and options
def cfCRstatus = getFieldById('customfield_34803')
def cfCRstatusConfig = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(34803).getRelevantConfig(getIssueContext())
def cfCRstatusOptions = ComponentAccessor.getOptionsManager().getOptions(cfCRstatusConfig)

//List of allowed options
def allowedOptions

//For a particular status, the list of "CR status" field values should be different
switch(currentStatus) {
    //Open
    case '1':
        allowedOptions = []
        break
    //Ready For Implementation
    case '22136':
        allowedOptions = cfCRstatusOptions.findAll { option -> option.getValue() in ['cr-status:ddo-approval'] }
        break
    //In progress
    case '3':
        allowedOptions = cfCRstatusOptions.findAll { option -> option.getValue() in ['cr-status:ddo-approval',
                                                                                     'cr-status:system-approval',
                                                                                     'cr-status:impact-analysis',
                                                                                     'cr-status:wait-for-deploy-int',
                                                                                     'cr-status:deployed-int',
                                                                                     'cr-status:testing-int',
                                                                                     'cr-status:testing-int-eco',
                                                                                     'cr-status:notification',
                                                                                     'cr-status:wait-for-deploy-prod'] }
        break
    //Resolved
    case '5':
        allowedOptions = cfCRstatusOptions.findAll { option -> option.getValue() in ['cr-status:wait-for-deploy-prod',
                                                                                     'cr-status:deployed-prod',
                                                                                     'cr-status:testing-prod'] }
        break
    //On Hold
    case '10000':
        allowedOptions = cfCRstatusOptions.findAll { option -> option.getValue() in ['cr-status:ddo-approval',
                                                                                     'cr-status:system-approval',
                                                                                     'cr-status:impact-analysis',
                                                                                     'cr-status:wait-for-deploy-int',
                                                                                     'cr-status:deployed-int',
                                                                                     'cr-status:testing-int',
                                                                                     'cr-status:testing-int-eco',
                                                                                     'cr-status:notification',
                                                                                     'cr-status:wait-for-deploy-prod'] }
        break
    //Blocked
    case '10079':
        allowedOptions = cfCRstatusOptions.findAll { option -> option.getValue() in ['cr-status:ddo-approval',
                                                                                     'cr-status:system-approval',
                                                                                     'cr-status:impact-analysis',
                                                                                     'cr-status:wait-for-deploy-int',
                                                                                     'cr-status:deployed-int',
                                                                                     'cr-status:testing-int',
                                                                                     'cr-status:testing-int-eco',
                                                                                     'cr-status:notification',
                                                                                     'cr-status:wait-for-deploy-prod',
                                                                                     'cr-status:deployed-prod',
                                                                                     'cr-status:testing-prod'] }
        break
    //Reopened
    case '4':
        allowedOptions = cfCRstatusOptions.findAll { option -> option.getValue() in ['cr-status:ddo-approval',
                                                                                     'cr-status:system-approval',
                                                                                     'cr-status:impact-analysis',
                                                                                     'cr-status:wait-for-deploy-int',
                                                                                     'cr-status:deployed-int',
                                                                                     'cr-status:testing-int',
                                                                                     'cr-status:testing-int-eco',
                                                                                     'cr-status:notification',
                                                                                     'cr-status:wait-for-deploy-prod'] }
        break
    //Rejected
    case '10188':
        allowedOptions = []
        break
    //Closed
    case '6':
        allowedOptions = cfCRstatusOptions.findAll { option -> option.getValue() in ['cr-status:closed'] }
        break
}

if (cfCRstatus) {
    cfCRstatus.setFieldOptions(allowedOptions)
}
Volodymyr December 13, 2023

How to display only unreleased "Fix Version/s|Affects Version/s" options (Behaviours)

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.customfields.option.Option
import com.atlassian.jira.project.version.VersionManager

def versionManager = ComponentAccessor.getVersionManager()

def fixVersions = getFieldById('fixVersions')
def affectsVersions = getFieldById('versions')

def projectId = issueContext.getProjectId()
def archivedVersions = false

def versions = versionManager.getVersionsUnreleased(projectId, archivedVersions)

fixVersions.setFieldOptions(versions)
affectsVersions.setFieldOptions(versions)
Volodymyr December 13, 2023

How to limit "Linked Issue" field options (Behaviours)

import com.onresolve.jira.groovy.user.FormField
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.link.IssueLinkTypeManager
import com.onresolve.jira.groovy.user.FieldBehaviours
import org.apache.log4j.Logger
import org.apache.log4j.Level
import groovy.transform.BaseScript

@BaseScript FieldBehaviours fieldBehaviours
def log = Logger.getLogger(getClass())
log.setLevel(Level.DEBUG)

// "Issue Links" or "Linked Issues" field of the issue
FormField issueLinks = getFieldById('issuelinks-linktype')

// Outward links that are needed
def outwardLinkNames = ['associated with',
                        'blocked with',
                        'discovered while testing',
                        'implemented in',
                        'clones',
                        'is cloned by',
                        'includes',
                        'continues',
                        'covers',
                        'depends on',
                        'duplicates',
                        'uses',
                        'split to',
                        'contains',
                        'causes',
                        'relates to',
                        'resolves']
  

// Inward links that are needed
def inwardLinkNames = ['associated with',
                       'blocking',
                       'testing discovered',
                       'fixed in',
                       'is cloned by',
                       'clones',
                       'is included in',
                       'is continued by',
                       'covered by',
                       'is dependent on',
                       'is duplicated by',
                       'is used by',
                       'split from',
                       'is a part of',
                       'is caused by',
                       'relates to',
                       'is resolved by']

IssueLinkTypeManager issueLinkTypeManager = ComponentAccessor.getComponent(IssueLinkTypeManager)

// Get the outward link names you need by matches with existing
def allowedOutwardLinks = issueLinkTypeManager.getIssueLinkTypes(false).findAll {
    it.getOutward() in outwardLinkNames }
    .collectEntries { [it.getOutward(), it.getOutward()] }

// Get the inward link names you need by matches with existing
def allowedInwardLinks = issueLinkTypeManager.getIssueLinkTypes(false).findAll {
    it.getInward() in inwardLinkNames }
    .collectEntries { [it.getInward(), it.getInward()] }

// Combine all the outward and inward link names you need
def allowedIssueLinks = allowedOutwardLinks << allowedInwardLinks
log.debug("Allowed issue links: ${allowedIssueLinks}")

// The options for the 'issuelinks' field have to be set in this structure: [blocks:blocks, relates to:relates to]
// becase the HTML structure of the field uses the actual link direction name as the value property.
issueLinks.setFieldOptions(allowedIssueLinks)
Volodymyr December 13, 2023

How to set a default value for "Resolution" field on the pop-up screen when moving to a specific status (Behaviours)

/*
How it works:
When moving to the following statuses, the corresponding resolutions should be specified as a default value:
- Rejected - preselected by default if the issue is moved to Rejected status
- Blocked - preselected by default if the issue is moved to Blocked status
- Done - preselected by default if the issue is moved to Resolved/Verified/Closed statuses + Ready for Testing, Testing.

The script is run when a transition occurs, i.e., a change in status.
We retrieve the workflow that the issue uses, and if one of the listed transitions occurs
where there is a screen with the Resolution field, then we set the default value for the field.
*/

import com.atlassian.jira.component.ComponentAccessor
import org.apache.log4j.Level
import org.apache.log4j.Logger
import com.onresolve.jira.groovy.user.FieldBehaviours
import groovy.transform.BaseScript

if (!underlyingIssue) {
    return
}

@BaseScript FieldBehaviours fieldBehaviours
def log = Logger.getLogger(getClass())
log.setLevel(Level.ERROR)

if (getAction() != null) {
    // ID of the current transition
    def transitionId = getAction().id
    // Resolution field
    def resoltuionField = getFieldById('resolution')
    // Name of the current issue's workflow
    def workflowName = ComponentAccessor.getWorkflowManager().getWorkflow(underlyingIssue).name

    // List of available resolutions for the issue
    String listOfAvailabeResolutions = getAction()?.getMetaAttributes()?.get('jira.field.resolution.include')
    // Split the list of resolutions
    def splitListOfAvailabeResolutions = listOfAvailabeResolutions.split(',')

    switch(workflowName) {
        case 'EPMBSRV Workflow':
            switch(transitionId) {
                // To status Resolved
                case 21:
                // To status Closed
                case 31:
                    resoltuionField.setFormValue(splitListOfAvailabeResolutions.find { it.contains('6') }) // Done
                    break
            }
            break
        case 'EPMBSRV Bug Workflow':
            switch(transitionId) {
                // To status Rejected
                case 71:
                    resoltuionField.setFormValue(splitListOfAvailabeResolutions.find { it.contains('9') }) // Rejected
                    break
                // To status Blocked
                case 61:
                    resoltuionField.setFormValue(splitListOfAvailabeResolutions.find { it.contains('11000') }) // Blocked
                    break
                // To status Verified
                case 51:
                // To status Closed
                case 11:
                    resoltuionField.setFormValue(splitListOfAvailabeResolutions.find { it.contains('6') }) // Done
                    break
            }
            break
        case 'EPMBSRV Change Request Workflow':
            switch(transitionId) {
                // To status Resolved
                case 21:
                // To status Closed
                case 31:
                    resoltuionField.setFormValue(splitListOfAvailabeResolutions.find { it.contains('6') }) // Done
                    break
            }
            break
        case 'EPMBSRV Story Workflow':
            switch(transitionId) {
                // To status Blocked
                case 91:
                    resoltuionField.setFormValue(splitListOfAvailabeResolutions.find { it.contains('11000') }) // Blocked
                    break
                // To status Resolved
                case 81:
                // To status Closed
                case 11:
                // To status "Ready for Testing"
                case 61:
                    resoltuionField.setFormValue(splitListOfAvailabeResolutions.find { it.contains('6') }) // Done
                    break
            }
            break
        case 'EPMBSRV Task Workflow':
            switch(transitionId) {
                // To status Closed
                case 31:
                    resoltuionField.setFormValue(splitListOfAvailabeResolutions.find { it.contains('6') }) // Done
                    break
            }
            break
    }
}
Volodymyr December 13, 2023

How to unhide customfields depending on a value of another customfield (Behaviours)

If customfield "Resolved with AI"(Checkbox) has any value except "true", hide additional fields, and if the value is only "true", show 4 additional fields and make them required.

def cfResolved_with_AIValue = getFieldById('customfield_35600').getValue()

def cfAI_Interaction_Time = getFieldById('customfield_27701')
def cfAI_Tool = getFieldById('customfield_14900')
def cfArea_of_AI_Solutioning = getFieldById('customfield_35602')
def cfProductivity_Impact = getFieldById('customfield_35601')

if (cfResolved_with_AIValue == 'true') {
    cfAI_Interaction_Time.setHidden(false)
    cfAI_Tool.setHidden(false)
    cfArea_of_AI_Solutioning.setHidden(false)
    cfProductivity_Impact.setHidden(false)

    cfAI_Interaction_Time.setRequired(true)
    cfAI_Tool.setRequired(true)
    cfArea_of_AI_Solutioning.setRequired(true)
    cfProductivity_Impact.setRequired(true)
} else {
    cfAI_Interaction_Time.setHidden(true)
    cfAI_Tool.setHidden(true)
    cfArea_of_AI_Solutioning.setHidden(true)
    cfProductivity_Impact.setHidden(true)

    cfAI_Interaction_Time.setRequired(false)
    cfAI_Tool.setRequired(false)
    cfArea_of_AI_Solutioning.setRequired(false)
    cfProductivity_Impact.setRequired(false)

    cfAI_Interaction_Time.setFormValue(null)
    cfAI_Tool.setFormValue(null)
    cfArea_of_AI_Solutioning.setFormValue(null)
    cfProductivity_Impact.setFormValue(null)
}
Volodymyr December 13, 2023

How to prevent more values from being selected in multi-select fields (Behaviours)

  • You need to add a script for a field that will prevent a user from selecting more than a specified value.
  • The number can be changed to a valid positive number (i.e., 1, 5, 15).
  • The error message can be changed.
// Works for Component/s field; change to your multi-select field like Fix Version/s, not Labels!
def componentsField = getFieldById('components')
def componentsValues = componentsField.getValue() as Collection

if (componentsValues.size() > 1) {
    componentsField.setValid(false)
    componentsField.setError('No more than one value!')
} else {
    componentsField.setValid(true)
    componentsField.clearError()
}
Volodymyr December 14, 2023

How to create a copy of a worklog from Issue to its Epic; updating or deleting such a worklog does the same in Epic (Listener)

/*
When one of the specified events: WorklogCreatedEvent, WorklogUpdatedEvent, WorklogDeletedEvent - occurs,
the current listener runs, which works only for issues that have "Epic Link".
When a worklog is created in an issue, the same worklog is created in the issue's Epic.
Updating or deleting this worklog updates or deletes the same worklog in the Epic.
*/

import com.atlassian.jira.security.roles.ProjectRole
import com.atlassian.jira.issue.worklog.WorklogImpl2
import com.atlassian.jira.event.worklog.WorklogDeletedEvent
import com.atlassian.jira.event.worklog.WorklogUpdatedEvent
import com.atlassian.jira.event.worklog.WorklogCreatedEvent
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.component.ComponentAccessor

// Current worklog and "Epic Link" field
def worklog = event?.getWorklog()
def issue = worklog?.getIssue()
def cfEpicLink = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(14500)

// If the worklog is in Epic or issue that doesn't have "Epic Link" - exit the script
if (issue.getIssueTypeId() == '6' ||  !issue.getCustomFieldValue(cfEpicLink)) {
    return
}

// Epic of the issue and current user
def epicOfIssue = issue.getCustomFieldValue(cfEpicLink) as MutableIssue
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Managers
def worklogManager = ComponentAccessor.getWorklogManager()

// Check what happens with the worklog; it is created, updated, or deleted.
if (event instanceof WorklogCreatedEvent) {
    def newWorklogForEpic = new WorklogImpl2(epicOfIssue,
                                             worklog.id + 1,
                                             worklog.authorKey,
                                             worklog.comment,
                                             worklog.startDate,
                                             worklog.groupLevel,
                                             worklog.roleLevelId,
                                             worklog.timeSpent,
                                             worklog.getAt('projectRole') as ProjectRole)

    worklogManager.create(currentUser, newWorklogForEpic, 0, true)
} else if (event instanceof WorklogUpdatedEvent) {
    def worklogToUpdateForEpic = new WorklogImpl2(epicOfIssue,
                                                  worklog.id + 1,
                                                  worklog.authorKey,
                                                  worklog.comment,
                                                  worklog.startDate,
                                                  worklog.groupLevel,
                                                  worklog.roleLevelId,
                                                  worklog.timeSpent,
                                                  worklog.updateAuthorKey,
                                                  worklog.created,
                                                  worklog.updated,
                                                  worklog.getAt('projectRole') as ProjectRole)

    if (worklogToUpdateForEpic && worklogToUpdateForEpic.getIssue() == epicOfIssue) {
        worklogManager.update(currentUser, worklogToUpdateForEpic, 0, true)
    }
} else if (event instanceof WorklogDeletedEvent) {
    def worklogToDeleteForEpic = worklogManager?.getById(worklog.id + 1)

    if (worklogToDeleteForEpic && worklogToDeleteForEpic.getIssue() == epicOfIssue) {
        worklogManager.delete(currentUser, worklogToDeleteForEpic, 0, true)
    }
}
Volodymyr December 14, 2023

How to sum Sub-tasks' "Story Points" to the parent (Listener)

/*
When one of the specified events: Issue Created, Issue Updated, Issue Deleted - occurs,
the current listener runs, which works only for Sub-task issue type whose parent is Story.
If a new Sub-task is created with a value for "Story Points" field, that value is added to a temporary variable,
and the value from other Sub-tasks is added to it. When a Sub-task is created, updated, deleted,
the listener retrieves the parent of that Sub-task and retrieves all its Sub-tasks,
checks the value of "Story Points" field for each sub-task, and if there is one, adds it to the temporary variable.
After it, the temporary variable is set as a new value in the parent, and the parent is updated.
*/

import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.event.type.EventType
import com.atlassian.jira.component.ComponentAccessor

// Current Issue
def issue = event?.issue

// Recalculation of the value occurs if the event occurred in Sub-task(5) with Story(7) parent
if (issue.issueTypeId == '5' && issue.getParentObject().issueTypeId == '7') {
   // Current user who did the action and customfield "Story Points"
   def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
   def cfStoryPoints = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(10004)

   Double total = 0

   // If a new Sub-task with a value for the field will be created, we save it as the first one
   if (event.getEventTypeId() == EventType.ISSUE_CREATED_ID) {
      def cfStoryPointsCreationValue = issue.getCustomFieldValue(cfStoryPoints)
      total += cfStoryPointsCreationValue != null ? cfStoryPointsCreationValue : 0
   }

   def parentIssue = issue.getParentObject() as MutableIssue
   def subTasks = parentIssue.getSubTaskObjects()

   subTasks.each {
      def cfStoryPointsValue = (Double) it.getCustomFieldValue(cfStoryPoints)
      if (cfStoryPointsValue) {
         total += cfStoryPointsValue
      }
   }

   //Set a new value for the field and update the parent
   parentIssue.setCustomFieldValue(cfStoryPoints, total)
   ComponentAccessor.getIssueManager().updateIssue(currentUser, parentIssue, EventDispatchOption.DO_NOT_DISPATCH, false)
}
Volodymyr December 14, 2023

How to transition issues to the final status when one of its fix versions is released (Listener)

/*
The script is triggered when a version is released in the project - VersionReleaseEvent,
after that, a JQL query is run that checks for issues on the project
that is not in status Done/Closed and with a value/values for "Fix Version/s" field.
Every issue is checked separately, and if the ticket has a value that has been released,
it is automatically transitioned to the final status.
If there are more values than the released version, keep it and delete others.
*/

import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.IssueInputParametersImpl
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.component.ComponentAccessor

// ID of the released version and current user
def versionId = event?.getVersionId()
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Managers
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)
def versionManager = ComponentAccessor.getVersionManager()
def workflowManager = ComponentAccessor.getWorkflowManager()
def issueService = ComponentAccessor.getIssueService()
def issueManager = ComponentAccessor.getIssueManager()

// JQL query and Search to retrieve all issues with a value for "Fix Version/s" field
// Change to your query: returning tickets that need to be transitioned
def query = jqlQueryParser.parseQuery("project = EPMHRMS AND status NOT IN (Done, Closed) AND fixVersion IS NOT EMPTY")
def search = searchService.search(currentUser, query, PagerFilter.getUnlimitedFilter())
def searchResults = search.getResults().findAll()

// Checking every ticket
searchResults?.each {
    def issue = issueManager.getIssueObject(it.id)
    def issueTypeId = issue.getIssueTypeId()

    // Values of "Fix Version/s" field
    def versionsOfIssue = versionManager.getFixVersionsFor(issue)

    // Checking every version of the issue
    versionsOfIssue?.each { version ->
        // If the ticket contains the released version, transition it to the final status
        if (version.id == versionId) {
            int actionID

            switch (issueTypeId) {
                // Change Request
                case '42':
                    actionID = 171
                    break
                // Bug
                case '1':
                    actionID = 341
                    break
                // Epic
                case '6':
                    actionID = 291
                    break
                // Improvement
                case '4':
                    actionID = 111
                    break
                // Initiative
                case '12600':
                    actionID = 311
                    break
                // Spike
                case '150':
                    actionID = 141
                    break
                // Story
                case '7':
                    actionID = 921
                    break
                // Sub-bug
                case '78':
                    actionID = 171
                    break
                // Sub-task
                case '5':
                    actionID = 161
                    break
                // Task
                case '3':
                    actionID = 261
                    break
                // Issue
                case '10':
                    actionID = 221
                    break
                // Technical task
                case '8':
                    actionID = 171
                    break
                // BA Task ST
                // Design Task ST
                // QA Task ST
                case '82':
                case '10800':
                case '13':
                    actionID = 161
                    break
                // Build
                // Feature
                // Milestone
                // Other
                // Requirement
                // Service Request
                // Support Request
                // Test
                // Topic
                case '33':
                case '17':
                case '10700':
                case '71':
                case '99':
                case '106':
                case '58':
                case '105':
                case '31':
                    actionID = 171
                    break
                default:
                    break
            }

            // If actionID returns a valid result (in our case isn't null),
            // then transition the issue to the final status and set/leave only the released version
            if (actionID) {
                def transitionValidationResult = issueService.validateTransition(currentUser, issue.id, actionID, new IssueInputParametersImpl())
                issueService.transition(currentUser, transitionValidationResult)

                def versionToSet = [] as Collection
                versionToSet.add(versionsOfIssue?.find { it.id == versionId })

                issue?.setFixVersions(versionToSet)
                issue?.setResolutionId('6') // Set resolution to Done
                issueManager.updateIssue(currentUser, issue, EventDispatchOption.ISSUE_UPDATED, false)
            }
        }
    }
}
Volodymyr December 14, 2023

The following scripts are a small family, as I did them for one project:

How to sum up "Original Estimate" values from issues in the Epic and set their sum to it (Listener)

/*
When one of the specified events: Issue Created, Issue Updated, Issue Deleted - occurs,
the current listener runs, which works only for issues that have "Epic Link".
If a new issue is created with a value for "Epic Link" field, the value for "Original Estimate"
is added to a temporary variable, and values from other issues are added to it.
When an issue with "Epic Link" value is created, updated, deleted, the listener retrieves
the Epic of that issue and retrieves all issues in it, checks the value of "Original Estimate"
for every issue, and if there is one, adds it to the temporary variable. After that, the temporary
variable is set as a new value in the Epic, and it is updated. The value only changes in Epic
if it is different from the current one.
*/

import com.atlassian.jira.event.type.EventType
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.component.ComponentAccessor

// Curent issue and "Epic Link" field
def issue = event?.issue
def cfEpicLink = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(14500)

// If issue type is Epic or it doesn't have Epic Link - exit the script
if (issue.getIssueTypeId() == '6' || !issue.getCustomFieldValue(cfEpicLink)) {
    return
}

// Epic of the issue and current user
def epicOfIssue = issue.getCustomFieldValue(cfEpicLink) as MutableIssue
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Managers
def issueManager = ComponentAccessor.getIssueManager()

// Temporary variable for the sum of values from "Original Estimate" field.
Long originalEstimateTotal = 0

// If the issue is created with the value for "Epic Link"
if (event?.getEventTypeId() == EventType.ISSUE_CREATED_ID) {
    !issue.getOriginalEstimate() ?: (originalEstimateTotal += issue.getOriginalEstimate())
}

// Managers for JQL search
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)

// JQL query and Search to retrieve all issues in Epic
def query = jqlQueryParser.parseQuery("issueFunction in issuesInEpics(\'key = ${epicOfIssue.key}\')")
def search = searchService.search(currentUser, query, PagerFilter.getUnlimitedFilter())
def searchResults = search.getResults().findAll()

searchResults?.each {
    !it.getOriginalEstimate() ?: (originalEstimateTotal += it.getOriginalEstimate())
}

if (epicOfIssue.getOriginalEstimate() == originalEstimateTotal) {
    return
} else if (epicOfIssue.getOriginalEstimate() != originalEstimateTotal) {
    epicOfIssue.setOriginalEstimate(originalEstimateTotal)
    issueManager.updateIssue(currentUser, epicOfIssue, EventDispatchOption.ISSUE_UPDATED, false)
}

How to sum up "Original Estimate" values from sub-tasks and set their sum to the parent (Listener)

/*
When one of the specified events: Issue Created, Issue Updated, Issue Deleted - occurs,
the current listener runs, which works only for sub-tasks. If a new sub-task is created with a value for "Original Estimate" field,
it's added to a temporary variable, and values from other sub-tasks of the parent are added to it.
After that, the temporary variable is set as a new value in the parent, and it is updated.
The value only changes if it is different from the current one.
*/

import com.atlassian.jira.event.type.EventType
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.component.ComponentAccessor

def issue = event?.issue

// If the issue is not a sub-task - exit the script
if (!issue.isSubTask()) {
    return
}

// Current user
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Managers
def issueManager = ComponentAccessor.getIssueManager()

// Temporary variable for the sum of values from "Original Estimate" field
Long originalEstimateTotal = 0

// If the issue is created with the value for "Original Estimate"
if (event?.getEventTypeId() == EventType.ISSUE_CREATED_ID) {
    !issue.getOriginalEstimate() ?: (originalEstimateTotal += issue.getOriginalEstimate())
}

// Retrieve the parent of the current sub-task
def parentOfIssue = issueManager.getIssueObject(issue.getParentId())

// Retrieve the value for the field "Original Estimate" of each sub-task from the parent
// and if there is a value, add it to the variable "originalEstimateTotal"
parentOfIssue.getSubTaskObjects()?.each { subtask ->
    !subtask.getOriginalEstimate() ?: (originalEstimateTotal += subtask.getOriginalEstimate())
}

// If the value of the parent is not equal to the total value "originalEstimateTotal",
// then set this value in the parent
if (parentOfIssue.getOriginalEstimate() == originalEstimateTotal) {
    return
} else if (parentOfIssue.getOriginalEstimate() != originalEstimateTotal) {
    parentOfIssue.setOriginalEstimate(originalEstimateTotal)
    issueManager.updateIssue(currentUser, parentOfIssue, EventDispatchOption.ISSUE_UPDATED, false)
}

How to sum up "Original Estimate" values from sub-tasks in a Story and set their sum to the Epic of the Story (Listener)

/*
When one of the specified events: Issue Created, Issue Updated, Issue Deleted - occurs,
the current listener runs, which works only for sub-tasks in a Story.
If a new sub-task is created with a value for "Original Estimate" field,
it's added to a temporary variable, and values from other sub-tasks of the parent are added to it.
After that, the temporary variable is set as a new value in the Epic of the parent, and it is updated.
The value only changes if it is different from the current one.
*/

import com.atlassian.jira.event.type.EventType
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.component.ComponentAccessor

// Customfield "Epic Link" and "Issue Manager"
def cfEpicLink = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(14500)
def issueManager = ComponentAccessor.getIssueManager()

// Issue, its parent, Epic of the parent
def issue = event?.issue
def parentOfIssue = issue?.getParentObject()
def epicOfParent = issueManager.getIssueObject(parentOfIssue?.getCustomFieldValue(cfEpicLink)?.key)

// If the issue is NOT a Sub-task, its parent is NOT a Story and the parent does NOT have a value for "Epic Link" field - exit the script
if (!(issue.isSubTask() && parentOfIssue.getIssueTypeId() == '7' && parentOfIssue.getCustomFieldValue(cfEpicLink))) {
    return
}

// Current user and the "Planned effort" field
def cfPlannedEffort = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(34732)
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

Double originalEstimateTotal = 0

// If the issue is created with the value for "Original Estimate" field
if (event?.getEventTypeId() == EventType.ISSUE_CREATED_ID) {
    !issue.getOriginalEstimate() ?: (originalEstimateTotal += issue.getOriginalEstimate())
}

// Managers for JQL search
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)

// JQL query and Search to retrieve all issues in the Epic
def query = jqlQueryParser.parseQuery("issueFunction in issuesInEpics(\'key = ${epicOfParent.key}\')")
def search = searchService.search(currentUser, query, PagerFilter.getUnlimitedFilter())
def searchResults = search.getResults().findAll()

// Checking every ticket in the epic and only continue if it's Story
searchResults?.each { issueInEpic ->
    if (issueInEpic.getIssueTypeId() == '7') {
        def subtasks = issueInEpic?.getSubTaskObjects()

        // If the Story has sub-tasks, retrieve the value for "Original Estimate" field from each one
        if (subtasks) {
            subtasks.each { subtask ->
                !subtask.getOriginalEstimate() ?: (originalEstimateTotal += subtask.getOriginalEstimate())
            }
        }
    }
}

// If the Epic's value doesn't equal to originalEstimateTotal, set it to the Epic
if (epicOfParent.getCustomFieldValue(cfPlannedEffort) == (originalEstimateTotal/3600).toDouble()) {
    return
} else if (epicOfParent.getCustomFieldValue(cfPlannedEffort) != (originalEstimateTotal/3600).toDouble()) {
    epicOfParent.setCustomFieldValue(cfPlannedEffort, (originalEstimateTotal/3600).toDouble())
    issueManager.updateIssue(currentUser, epicOfParent, EventDispatchOption.ISSUE_UPDATED, false)
}
Volodymyr December 14, 2023

The following scripts are a small family, as I did them for one project:

How to sum up worklogs from issues in the Epic and set their sum to it (Listener)

/*
It runs when there are three events: WorlogCreatedEvent, WorklogUpdatedEvent, WorklogDeletedEvent.
If a ticket has an epic, all tickets from the epic are retrieved, and the worklogs of every issue are added to the temporary variable,
and the total sum is added to the epic in a number field.
*/

import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.component.ComponentAccessor

// Curent worklog, issue and "Epic Link" field
def worklog = event?.getWorklog()
def issue = worklog?.getIssue()
def cfEpicLink = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(14500)

// If issue type is Epic or it doesn't have Epic Link - exit the script
if (issue.getIssueTypeId() == '6' || !issue.getCustomFieldValue(cfEpicLink)) {
    return
}

// Epic of the issue, its "Progress" field, and current user
def epicOfIssue = issue.getCustomFieldValue(cfEpicLink) as MutableIssue
def cfProgress = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(35906)
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Managers
def issueManager = ComponentAccessor.getIssueManager()

// Temporary variable for the sum of worklogs
Double timeSpentTotal = 0

// Managers for JQL search
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)

// JQL query and Search to retrieve all issues in Epic
def query = jqlQueryParser.parseQuery("issueFunction in issuesInEpics(\'key = ${epicOfIssue.key}\')")
def search = searchService.search(currentUser, query, PagerFilter.getUnlimitedFilter())
def searchResults = search.getResults().findAll()

searchResults?.each {
    !it?.getTimeSpent() ?: (timeSpentTotal += it.getTimeSpent())
}

if (epicOfIssue.getCustomFieldValue(cfProgress) != (timeSpentTotal/3600).toDouble()) {
    epicOfIssue.setCustomFieldValue(cfProgress, (timeSpentTotal/3600).toDouble())
    issueManager.updateIssue(currentUser, epicOfIssue, EventDispatchOption.ISSUE_UPDATED, false)
}


How to sum up worklogs from sub-tasks and set their sum to the parent (Listener)

/*
It runs when there are three events: WorlogCreatedEvent, WorklogUpdatedEvent, WorklogDeletedEvent.
If a ticket is Sub-task, all tickets from the parent are retrieved, and the worklogs of every issue are added to the temporary variable,
and the total sum is added to the parent in a number field.
*/

import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.component.ComponentAccessor

// Current issue
def issue = event.getWorklog()?.issue

// If the issue is not a sub-task - exit the script
if (!issue.isSubTask()) {
    return
}

// "Progress" field and the current user
def cfProgress = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(34731)
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Managers
def issueManager = ComponentAccessor.getIssueManager()

// Temporary variable for the sum of worklogs
Double timeSpentTotal = 0

// Retrieve the parent of the current sub-task
def parentOfIssue = issueManager.getIssueObject(issue.getParentId())

// Retrieve the value for the field "Logged" time of each sub-task from the parent
// and if there is a value, add it to the variable "timeSpentTotal"
parentOfIssue.getSubTaskObjects()?.each { subtask ->
    !subtask.getTimeSpent() ?: (timeSpentTotal += subtask.getTimeSpent())
}

// If the value of the parent is not equal to the total value "timeSpentTotal",
// then set this value in the parent
if (parentOfIssue.getCustomFieldValue(cfProgress) != (timeSpentTotal/3600).toDouble()) {
    parentOfIssue.setCustomFieldValue(cfProgress, (timeSpentTotal/3600).toDouble())
    issueManager.updateIssue(currentUser, parentOfIssue, EventDispatchOption.ISSUE_UPDATED, false)
}


How to sum up worklogs from sub-tasks in a Story and set their sum to the Epic of the Story (Listener)

/*
It runs when there are three events: WorlogCreatedEvent, WorklogUpdatedEvent, WorklogDeletedEvent.
If a ticket is Sub-task, its parent is Story and it has value for "Epic Link" field,
all tickets from the epic are retrieved, and the worklogs of every sub-task in Stories only are added to the temporary variable,
and the total sum is added to the Epic in a number field.
*/

import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.component.ComponentAccessor

// Customfield 'Epic Link' and 'Issue Manager'
def cfEpicLink = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(14500)
def issueManager = ComponentAccessor.getIssueManager()

// Issue, its parent, Epic of the parent
def issue = event.getWorklog()?.issue
def parentOfIssue = issueManager.getIssueObject(issue?.getParentId())
def epicOfParent = issueManager.getIssueObject(parentOfIssue?.getCustomFieldValue(cfEpicLink)?.key)

// If the issue is NOT a Sub-task, its parent is NOT a Story and the parent does NOT have a value for 'Epic Link' field - exit the script
if (!(issue.isSubTask() && parentOfIssue.getIssueTypeId() == '7' && parentOfIssue.getCustomFieldValue(cfEpicLink))) {
    return
}

// "Progress" field and the current user
def cfProgress = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(34731)
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Temporary variable for the sum of worklogs
Double timeSpentTotal = 0

// Managers for JQL search
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)

// JQL query and Search to retrieve all issues in the Epic
def query = jqlQueryParser.parseQuery("issueFunction in issuesInEpics(\'key = ${epicOfParent.key}\')")
def search = searchService.search(currentUser, query, PagerFilter.getUnlimitedFilter())
def searchResults = search.getResults().findAll()

searchResults?.each { issueInEpic ->
    if (issueInEpic.getIssueTypeId() == '7') {
        def subtasks = issueInEpic?.getSubTaskObjects()

        if (subtasks) {
            subtasks.each { subtask ->
                !subtask.getTimeSpent() ?: (timeSpentTotal += subtask.getTimeSpent())
            }
        }
    }
}

// If the Epic's value doesn't equal to timeSpentTotal, set it to the Epic
if (epicOfParent.getCustomFieldValue(cfProgress) == (timeSpentTotal/3600).toDouble()) {
    return
} else if (epicOfParent.getCustomFieldValue(cfProgress) != (timeSpentTotal/3600).toDouble()) {
    epicOfParent.setCustomFieldValue(cfProgress, (timeSpentTotal/3600).toDouble())
    issueManager.updateIssue(currentUser, epicOfParent, EventDispatchOption.ISSUE_UPDATED, false)
}

 

Volodymyr December 14, 2023

Custom Script Field - WSJF

There was a different script earlier in the article that I modified slightly to work in one of my projects. The initial article:

How to prioritise issues in Jira using WSJF as Scripted Fields (adaptavist.com)

import com.atlassian.jira.component.ComponentAccessor

// Custom method to retrieve the customfield
def getCustomFieldValue(Long cfID) {
    return (Double) issue.getCustomFieldValue(ComponentAccessor.getCustomFieldManager().getCustomFieldObject(cfID))
}

// Using the method above to get the values of the customfield
final BISSNESS_VALUE = getCustomFieldValue(10005) // Business Value
final PRIORITY_RATE = getCustomFieldValue(17713) // Priority Rate
final RISK = getCustomFieldValue(13100) // Risk
final STORY_POINTS = getCustomFieldValue(10004) // Story Points

// If all the specified fields have a value, then display the result according to the formula below
if (BISSNESS_VALUE && PRIORITY_RATE && RISK && STORY_POINTS) {
    // It works, but shows "Cannot find matching method java.lang.Object#plus(java.lang.Object)"
    return (BISSNESS_VALUE + PRIORITY_RATE + RISK)/STORY_POINTS
} else {
    return null
}
Volodymyr December 14, 2023

How to add a comment with time in every status the issue has been

import com.atlassian.jira.datetime.DateTimeFormatter
import com.atlassian.jira.issue.history.ChangeItemBean
import com.atlassian.jira.component.ComponentAccessor

// One second is 1000 milliseconds
final int SECOND = 1000
final int MINUTE = 60 * SECOND
final int HOUR = 60 * MINUTE
final int DAY = 24 * HOUR

def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def newDateTimeFormat = ComponentAccessor.getComponent(DateTimeFormatter.class)

def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def changeItems = changeHistoryManager.getChangeItemsForField(issue, 'status')

List<Long> timeInEveryStatus = []
timeInEveryStatus.add(issue.created.time)
// Time in every status
changeItems.each { ChangeItemBean item ->
    timeInEveryStatus.add(item.created.time)
}

// Values for the comment
BigDecimal addNumberToComment = 0
Long issueCreatedTime = 0
StringBuffer newComment = new StringBuffer('')

// Values for counting time in statuses and the first transition
long ms
int i = 0

// Every status change is checked
changeItems.each { ChangeItemBean item ->
    newComment.append("Date and time when the status was changed: ${newDateTimeFormat.format(item.created)}\n")
    newComment.append("From: ${item.getFromString()}\n")
    newComment.append("To: ${item.getToString()}\n")

    ms = timeInEveryStatus[i+1] - timeInEveryStatus[i]
    i += 1

    StringBuffer textToCommet = new StringBuffer('')
    if (ms > DAY) {
        addNumberToComment = ms / DAY
        textToCommet.append(addNumberToComment.toInteger()).append('d ')
        ms %= DAY
    }
    if (ms > HOUR) {
        addNumberToComment = ms / HOUR
        textToCommet.append(addNumberToComment.toInteger()).append('h ')
        ms %= HOUR
    }
    if (ms > MINUTE) {
        addNumberToComment = ms / MINUTE
        textToCommet.append(addNumberToComment.toInteger()).append('m ')
        ms %= MINUTE
    }
    if (ms > SECOND) {
        addNumberToComment = ms / SECOND
        textToCommet.append(addNumberToComment.toInteger()).append('s')
        ms %= SECOND
    }

    newComment.append("Time the issue was in status \"${item.getFromString()}\": ${textToCommet.toString()}\n")
    newComment.append('\n')
}

ComponentAccessor.getCommentManager().create(issue, currentUser, newComment.toString(), false)


The script is a copy of the above for testing in Script Console:

import com.atlassian.jira.datetime.DateTimeFormatter
import com.atlassian.jira.issue.history.ChangeItemBean
import com.atlassian.jira.component.ComponentAccessor

def issue = ComponentAccessor.getIssueManager().getIssueObject('ROYALPITA-16436')
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def newDateTimeFormat = ComponentAccessor.getComponent(DateTimeFormatter.class)

// One second is 1000 milliseconds
final int SECOND = 1000
final int MINUTE = 60 * SECOND
final int HOUR = 60 * MINUTE
final int DAY = 24 * HOUR

def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def changeItems = changeHistoryManager.getChangeItemsForField(issue, 'status')

List<Long> timeInEveryStatus = []
timeInEveryStatus.add(issue.created.time)

// Time in every status
changeItems.each { ChangeItemBean item ->
    timeInEveryStatus.add(item.created.time)
}

// Values for the comment
BigDecimal addNumberToComment = 0
Long issueCreatedTime = 0
StringBuffer newComment = new StringBuffer('')

// Values for counting time in statuses and the first transition
long ms
int i = 0

// Every status change is checked
changeItems.each { ChangeItemBean item ->
    log.warn("Date and time when the status was changed: ${newDateTimeFormat.format(item.created)}\n")
    log.warn("From: ${item.getFromString()}\n")
    log.warn("To: ${item.getToString()}\n")

    ms = timeInEveryStatus[i+1] - timeInEveryStatus[i]
    i += 1

    StringBuffer textToComment = new StringBuffer('')
    if (ms > DAY) {
        addNumberToComment = ms / DAY
        textToComment.append(addNumberToComment.toInteger()).append('d ')
        ms %= DAY

    }
    if (ms > HOUR) {
        addNumberToComment = ms / HOUR
        textToComment.append(addNumberToComment.toInteger()).append('h ')
        ms %= HOUR

    }
    if (ms > MINUTE) {
        addNumberToComment = ms / MINUTE
        textToComment.append(addNumberToComment.toInteger()).append('m ')
        ms %= MINUTE
    }
    if (ms > SECOND) {
        addNumberToComment = ms / SECOND
        textToComment.append(addNumberToComment.toInteger()).append('s')
        ms %= SECOND
    }

    log.warn("Time the issue was in status \"${item.getFromString()}\": ${textToComment.toString()}\n")
    log.warn('\n')
}
Volodymyr December 14, 2023

How to add a prefix to Summary based on a customfield of type select list

import com.atlassian.jira.component.ComponentAccessor

Long WHERE_ISSUED = 15201
String prefix

def cfWhereIssued = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(WHERE_ISSUED)
def cfWhereIssuedValue = issue.getCustomFieldValue(cfWhereIssued)

if (cfWhereIssuedValue) {
    switch (cfWhereIssuedValue) {
        case 'Development error':
            prefix = 'Development error'
            break
        case 'UI':
            prefix = 'UI'
            break
        case 'Functional':
            prefix = 'Functional'
            break
        case 'Specification changes':
            prefix = 'Specification changes'
            break
    }

    if (prefix) {
        issue.setSummary('[' + prefix + '] ' + issue.summary)
        def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
        ComponentAccessor.getIssueManager().updateIssue(currentUser, issue, EventDispatchOption.ISSUE_UPDATED, false)
    }
}
Volodymyr December 14, 2023

How to assign an issue to the first or last member of the group

import com.atlassian.jira.component.ComponentAccessor

def DL = 'WFT Business Desk Atlassian Support' // Replace the name to your DL

def groupManager = ComponentAccessor.getGroupManager()
def membersOfDL = groupManager.getUsersInGroup(DL, false) // without inactive users

if (membersOfDL) {
    // Use only one method and delete the other one
    // Assign to the first member
    issue.setAssignee(membersOfDL.first())

    // Assign to the last member
    issue.setAssignee(membersOfDL.last())
}
Volodymyr December 14, 2023

How to close an Epic when all its issues are Closed

import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.issue.IssueInputParametersImpl
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.component.ComponentAccessor

// Stop the script if the issue is Epic
if (issue.issueTypeId == '6') {
    return
}

// ID of "Epic link" field and current user
Long EPICLINK = 14500
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Get managers
def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def customfieldManager = ComponentAccessor.getCustomFieldManager()
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)
def workflowManager = ComponentAccessor.getWorkflowManager()
def issueService = ComponentAccessor.getIssueService()

// Epic of the current issue
def epicLinkCustomField = customfieldManager.getCustomFieldObject(EPICLINK)
String epicLink = issue.getCustomFieldValue(epicLinkCustomField)
def currentEpic = ComponentAccessor.getIssueManager().getIssueObject(epicLink)

// Stop the script If the issue has no Epic
if (!currentEpic) {
    return
}

// JQL to retrieve all issues in the Epic
def query = jqlQueryParser.parseQuery("issueFunction in issuesInEpics(\'key = ${currentEpic.key}\')")
def search = searchService.search(currentUser, query, PagerFilter.getUnlimitedFilter())
def searchResults = search.getResults().findAll{ it.getKey() != issue.getKey() } // Excluding the current issue, which goes to status "Closed"

// Epic's workflow
def workflow = workflowManager.getWorkflow(currentEpic)
int actionID = 21 // Resolved status
def transitionValidationResult = issueService.validateTransition(currentUser, currentEpic.id, actionID, new IssueInputParametersImpl())

// Transition the epic to Resolved status only if all issues in the epic are closed
if (searchResults.every{it.getStatusId() == '6'}) {
    String newComment = 'The issue is resolved. Please check all the updates and manually close it.'

    issueService.transition(currentUser, transitionValidationResult)
    ComponentAccessor.getCommentManager().create(currentEpic, currentUser, newComment, false)
}
Volodymyr December 14, 2023

If I recall correctly from my tests, creating an issue always requires three fields: Project (Where you're creating), Type (Is it a standard issue type or not?), Summary (Tickets should have a "name"); and creating a sub-task requires a fourth field - Parent (parent of the sub-task).

How to create an issue/How to create a sub-task

// How to create an issue
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.config.PriorityManager

def project = ComponentAccessor.getProjectManager().getProjectObjByKey('EPMDDOTEST') // Where to create
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser() // Who creates

def priority = ComponentAccessor.getComponent(PriorityManager).getPriorities().findByName('Critical').getId()
assert priority: 'There is no priority as Critical'

MutableIssue newIssue = ComponentAccessor.getIssueFactory().getIssue() // New issue
newIssue.setProjectObject(project)
newIssue.setIssueTypeId('1') // Type is Bug
newIssue.setSummary('Demo issue')
newIssue.setPriorityId(priority)
newIssue.setReporter(currentUser)

ComponentAccessor.getIssueManager().createIssueObject(currentUser, newIssue) // Creates a new issue

 

// How to create a sub-task
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.MutableIssue

def project = issue.getProjectObject() // Where to create
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser() // Who creates

def parentIssue = issue // Parent issue

MutableIssue newSubtask = ComponentAccessor.getIssueFactory().getIssue() // New issue
newSubtask.setProjectObject(project)
newSubtask.setIssueTypeId('5') // Type is Sub-task
newSubtask.setSummary('Demo sub-task')
newSubtask.setPriority(issue.getPriority())
newSubtask.setReporter(issue.getReporter())

ComponentAccessor.getIssueManager().createIssueObject(currentUser, newSubtask) // Creates a new issue
ComponentAccessor.getSubTaskManager().createSubTaskIssueLink(parentIssue, newSubtask, currentUser) // Creates a sub-task-issue link
Volodymyr December 14, 2023

How to create a sub-task for every member of the user picker custom field

import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.component.ComponentAccessor

// Project, parent issue, current user, customfield
// If Approvers customfield not empty - create new Approval sub-tasks and link with the parent
// Sub-tasks should be in a loop to create them for each member of the field

def APPROVERS = 33002 as Long
def projectKey = issue.getProjectObject().getKey()

// Get Managers
def projectManager = ComponentAccessor.getProjectManager()
def issueManager = ComponentAccessor.getIssueManager()
def issueFactory = ComponentAccessor.getIssueFactory()
def subtaskManager = ComponentAccessor.getSubTaskManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()

// Project, parent issue, current user, customfield
def project = projectManager.getProjectByCurrentKey(projectKey)
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def customFieldApprovers = customFieldManager.getCustomFieldObject(APPROVERS)

// Value of Approvers for the issue
def approversValue = customFieldApprovers.getValue(issue)

approversValue.each {
    def currentApprover = it as ApplicationUser

    // New subtask and its fields
    MutableIssue newSubtask = issueFactory.getIssue()
    newSubtask.setProjectObject(project)
    newSubtask.setIssueTypeId('13508')
    newSubtask.setSummary('[Approve] ' + '[' + currentApprover.getDisplayName() + ']')
    newSubtask.setReporter(issue.getReporter())
    newSubtask.setAssignee(currentApprover)
    newSubtask.setPriority(issue.getPriority())

    // Create a new subtask and link it to the parent
    issueManager.createIssueObject(currentUser, newSubtask)
    subtaskManager.createSubTaskIssueLink(issue, newSubtask, currentUser)
Volodymyr December 14, 2023

How to create a sub-task if a customfield is not null

import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.component.ComponentAccessor

// Create a sub-task if one of the customfield's boxes is ticked
def CHANGE_DATA_MODEL = 10100 as Long
def HISTORICAL_PATCHING = 34404 as Long
def project = issue.getProjectObject()

// Get Managers
def projectManager = ComponentAccessor.getProjectManager()
def issueManager = ComponentAccessor.getIssueManager()
def issueFactory = ComponentAccessor.getIssueFactory()
def subtaskManager = ComponentAccessor.getSubTaskManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()

// Current user, customfields
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def customField_CHANGE_DATA_MODEL = customFieldManager.getCustomFieldObject(CHANGE_DATA_MODEL)
def customField_HISTORICAL_PATCHING = customFieldManager.getCustomFieldObject(HISTORICAL_PATCHING)

// Values of customfields of the issue
def value_CHANGE_DATA_MODEL = customField_CHANGE_DATA_MODEL.getValue(issue)
def value_HISTORICAL_PATCHING = customField_HISTORICAL_PATCHING.getValue(issue)

if (value_CHANGE_DATA_MODEL != null) {
    // New subtask and its fields
    MutableIssue newSubtask = issueFactory.getIssue()
    newSubtask.setProjectObject(project)
    newSubtask.setIssueTypeId('13508')
    newSubtask.setSummary('[Change Data Model] for ' + '[' + issue.getSummary() + ']')
    newSubtask.setReporter(issue.getReporter())
    newSubtask.setPriority(issue.getPriority())

    // Create a new subtask and link it to the parent
    issueManager.createIssueObject(currentUser, newSubtask)
    subtaskManager.createSubTaskIssueLink(issue, newSubtask, currentUser)
}

if (value_HISTORICAL_PATCHING != null) {
    // New subtask and its fields
    MutableIssue newSubtask = issueFactory.getIssue()
    newSubtask.setProjectObject(project)
    newSubtask.setIssueTypeId('13508')
    newSubtask.setSummary('[Historical Patching] for ' + '[' + issue.getSummary() + ']')
    newSubtask.setReporter(issue.getReporter())
    newSubtask.setPriority(issue.getPriority())

    // Create a new subtask and link it to the parent
    issueManager.createIssueObject(currentUser, newSubtask)
    subtaskManager.createSubTaskIssueLink(issue, newSubtask, currentUser)
Volodymyr December 14, 2023

How to create a sub-task with values for customfields of list type (Select List (single choice))

import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.component.ComponentAccessor

// ID of the fields
Long CAUSE = 15200
Long CUSTOMER = 15109
Long ACTIVITY = 15111
Long STREAM = 33100

// Current user, project and parent issue
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def project = issue.getProjectObject()
def parentIssue = issue

// Managers
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def issueManager = ComponentAccessor.getIssueManager()
def optionsManager = ComponentAccessor.getOptionsManager()
def issueFactory = ComponentAccessor.getIssueFactory()

// Custom fields
def cfCause = customFieldManager.getCustomFieldObject(CAUSE)
def cfCustomer = customFieldManager.getCustomFieldObject(CUSTOMER)
def cfActivity = customFieldManager.getCustomFieldObject(ACTIVITY)
def cfStream = customFieldManager.getCustomFieldObject(STREAM)

// Config(context) of the custom fields
def cfCauseConfig = cfCause.getRelevantConfig(issue)
def cfCustomerConfig = cfCustomer.getRelevantConfig(issue)
def cfActivityConfig = cfActivity.getRelevantConfig(issue)
def cfStreamConfig = cfStream.getRelevantConfig(issue)

// OptionManager allows you to retrieve values for a specific config(context), for a specific field, for a specific issue
def cfCauseValue = optionsManager.getOptions(cfCauseConfig)?.find { it.toString() == 'Specification changes' }
def cfCustomerValue = optionsManager.getOptions(cfCustomerConfig)?.find { it.toString() == 'Blackberry [BBRY]' }
def cfActivityValue = optionsManager.getOptions(cfActivityConfig)?.find { it.toString() == 'API Documentation' }
def cfStreamValue = optionsManager.getOptions(cfStreamConfig)?.find { it.toString() == 'Test 3' }

// Creating a sub-task
MutableIssue newSubtask = issueFactory.getIssue()
newSubtask.setProjectObject(project)
newSubtask.setIssueTypeId('5') // Sub-task
newSubtask.setSummary('Sub-task for ' + '[' + issue.getSummary() + ']')
newSubtask.setPriority(issue.getPriority())
newSubtask.setReporter(issue.getReporter())
newSubtask.setCustomFieldValue(cfCause, cfCauseValue)
newSubtask.setCustomFieldValue(cfCustomer, cfCustomerValue)
newSubtask.setCustomFieldValue(cfActivity, cfActivityValue)
newSubtask.setCustomFieldValue(cfStream, cfStreamValue)

ComponentAccessor.getIssueManager().createIssueObject(currentUser, newSubtask) // Creates a new issue
ComponentAccessor.getSubTaskManager().createSubTaskIssueLink(parentIssue, newSubtask, currentUser) // Creates a sub-task-issue link

If you want to create a sub-task/s depending on the value of the Story Points field

import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.component.ComponentAccessor

def STORYPOINTS = 10004 as Long
def cfStotypointsValue = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(STORYPOINTS).getValue(issue) as Integer

if (cfStotypointsValue != null) {
    // ID of the fields
    Long CAUSE = 15200
    Long CUSTOMER = 15109
    Long ACTIVITY = 15111
    Long STREAM = 33100

    // Current user, project and parent issue
    def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
    def project = issue.getProjectObject()
    def parentIssue = issue

    // Managers
    def customFieldManager = ComponentAccessor.getCustomFieldManager()
    def issueManager = ComponentAccessor.getIssueManager()
    def optionsManager = ComponentAccessor.getOptionsManager()
    def issueFactory = ComponentAccessor.getIssueFactory()

    // Custom fields
    def cfCause = customFieldManager.getCustomFieldObject(CAUSE)
    def cfCustomer = customFieldManager.getCustomFieldObject(CUSTOMER)
    def cfActivity = customFieldManager.getCustomFieldObject(ACTIVITY)
    def cfStream = customFieldManager.getCustomFieldObject(STREAM)

    // Config(context) of the custom fields
    def cfCauseConfig = cfCause.getRelevantConfig(issue)
    def cfCustomerConfig = cfCustomer.getRelevantConfig(issue)
    def cfActivityConfig = cfActivity.getRelevantConfig(issue)
    def cfStreamConfig = cfStream.getRelevantConfig(issue)

    // OptionManager allows you to retrieve values for a specific config(context), for a specific field, for a specific issue
    def cfCauseValue = optionsManager.getOptions(cfCauseConfig)?.find { it.toString() == 'Specification changes' }
    def cfCustomerValue = optionsManager.getOptions(cfCustomerConfig)?.find { it.toString() == 'Blackberry [BBRY]' }
    def cfActivityValue = optionsManager.getOptions(cfActivityConfig)?.find { it.toString() == 'API Documentation' }
    def cfStreamValue = optionsManager.getOptions(cfStreamConfig)?.find { it.toString() == 'Test 3' }

    for (int i = 1; i <= cfStotypointsValue; i++) {
        // Creating a sub-task
        MutableIssue newSubtask = issueFactory.getIssue()
        newSubtask.setProjectObject(project)
        newSubtask.setIssueTypeId('5') // Sub-task
        newSubtask.setSummary('Sub-task ' + i + ' for ' + '[' + issue.getSummary() + ']')
        newSubtask.setPriority(issue.getPriority())
        newSubtask.setReporter(issue.getReporter())
        newSubtask.setCustomFieldValue(cfCause, cfCauseValue)
        newSubtask.setCustomFieldValue(cfCustomer, cfCustomerValue)
        newSubtask.setCustomFieldValue(cfActivity, cfActivityValue)
        newSubtask.setCustomFieldValue(cfStream, cfStreamValue)

        ComponentAccessor.getIssueManager().createIssueObject(currentUser, newSubtask) // Creates a new issue
        ComponentAccessor.getSubTaskManager().createSubTaskIssueLink(parentIssue, newSubtask, currentUser) // Creates a sub-task-issue link
    }
}
Volodymyr December 14, 2023

How to return a deleted value in the customfield

All scripts are run from the Console, but can be run in workflows; for that please delete the whole line starts "def issue"

I had a case when a project manager was moving issues from one project to another and forgot to check the box "Epic Link", so the value for the field was cleared for all moved issues. I had used "ChangeHistoryManager" before, so I quickly wrote a script that returned those values.

Single issue:

/*
A value for a customfield of type "Select List" has been deleted in the issue and needs to be set back.
In our case, the value for "Epic Status" field was deleted, so we check if it is available for the issue
and if the deleted value is equal to the one we retrieved.
*/

import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.component.ComponentAccessor

// The issue and its customfield that was removed
def issue = ComponentAccessor.getIssueManager().getIssueObject('ROYALPITA-16540') // Change the key
def customfield = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(14502) // Changed customfield
def customfieldConfig = customfield.getRelevantConfig(issue)
def neededOption = ComponentAccessor.getOptionsManager().getOptions(customfieldConfig).find { it.value == 'To Do' } // Change to the deleted value

def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def issueManager = ComponentAccessor.getIssueManager()
def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def changedItem = changeHistoryManager.getChangeItemsForField(issue, 'Epic Status') // Change to the customfield that was changed (the above one)

changedItem.each {
    def deletedValue = 'To Do' // Change to the deleted value

    if (it.getFromString()?.contains(deletedValue)) {
        issue.setCustomFieldValue(customfield, neededOption)
        issueManager.updateIssue(currentUser, issue, EventDispatchOption.ISSUE_UPDATED, false)
    }
}

 

/*
A value for a customfield of type "Text Field" has been deleted in the issue and needs to be set back.
In our case, the value for "External issue ID" field was deleted,
so we check for a match by the full value or a part of it.
*/

import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.component.ComponentAccessor

// The issue and its customfield that was removed
def issue = ComponentAccessor.getIssueManager().getIssueObject('ROYALPITA-16540') // Key of the ticket
def customfield = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(12203) // Changed customfield

def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def issueManager = ComponentAccessor.getIssueManager()
def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def changedItem = changeHistoryManager.getChangeItemsForField(issue, 'External issue ID') // Change to the customfield that was changed (the above one)

changedItem.each {
    def deletedValue = '7475' // Change to the deleted value or part of it

    if (it.getFromString()?.contains(deletedValue)) {
        issue.setCustomFieldValue(customfield, it.getFromString())
        issueManager.updateIssue(currentUser, issue, EventDispatchOption.ISSUE_UPDATED, false)
    }
}

 

/*
A value for a customfield of type "Number Field" has been deleted in the issue and needs to be set back.
In our case, the value for "Story Points" field was deleted, so we check for a match by the full value.
*/

import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.component.ComponentAccessor

// The issue and its customfield that was removed
def issue = ComponentAccessor.getIssueManager().getIssueObject('ROYALPITA-16540') // Key of the ticket
def customfield = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(10004) // Changed customfield

def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def issueManager = ComponentAccessor.getIssueManager()
def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def changedItem = changeHistoryManager.getChangeItemsForField(issue, 'Story Points') // Change to the customfield that was changed (the above one)

changedItem.each {
    def deletedValue = 235 // Change to the deleted value

    if (it.getFromString() == deletedValue.toString()) {
        issue.setCustomFieldValue(customfield, it.getFromString().toDouble())
        issueManager.updateIssue(currentUser, issue, EventDispatchOption.ISSUE_UPDATED, false)
    }
}

 Many issues:

/*
A value for "Epic Link" customfield has been deleted in the issues and needs to be set back.
In our case, we retrieve all issues from the JQL query, which can be modified at will to return values for only the required issues.
In this project, we check all project issues, and if the deleted epic contains the specified key, we return the field value.
*/

import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.component.ComponentAccessor

// Current user who will do the action and affected customfield
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def affectedField = ComponentAccessor.getCustomFieldManager().getCustomFieldObject(14500)

// Managers
def issueManager = ComponentAccessor.getIssueManager()
def customfieldManager = ComponentAccessor.getCustomFieldManager()
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)

// ChangeHistoryManager allows you to retrieve all changes in the issue
def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()

// Your JQL query and its validated representation
// Issues to check and return the deleted value for the field
def query = jqlQueryParser.parseQuery('project = ROYALPITA') // Please change to your query
def search = searchService?.search(currentUser, query, PagerFilter.getUnlimitedFilter())

/*
Note that getResults() returns read-only Issue objects and should not be used
where you need to update the issue. The method provides the issue key as text,
so we use IssueManager to retrieve it and basically convert text to an Issue object that can be modified.
*/

search.getResults().each {
    // One issue from the query at a time and its changes for customfield
    def issue = issueManager.getIssueObject(it?.getKey())
    def changedItem = changeHistoryManager.getChangeItemsForField(issue, 'Epic Link')

    changedItem?.each {
        def deletedItem = issueManager.getIssueObject(it.getFromString())

        if (it.getFromString()?.contains('ROYALPITA')) {
            issue.setCustomFieldValue(affectedField, deletedItem)
            issueManager.updateIssue(currentUser, issue, EventDispatchOption.ISSUE_UPDATED, false)
        }
    }
}

 

Volodymyr December 14, 2023

How to set a description depending on the issue type

String issueType = issue.getIssueType().getName()
String newDescription

switch (issueType) {
    case 'Change Request':
        newDescription = 'This is a Change Request'
        break
    case 'Epic':
        newDescription = 'This is an Epic, am I right?'
        break
    case 'Feature':
        newDescription = 'This is a Feature'
        break
    case 'Service Request':
        newDescription = 'This is a Service Request'
        break
    case 'Support Request':
        newDescription = 'This is a Support Request'
        break
    default:
        newDescription = 'This is a description for any other type of issue'
        break
}

issue.setDescription(newDescription)
Volodymyr December 14, 2023

How to set a new due date depending on the customfield value

Thanks to this webinar and the explanations

import com.atlassian.jira.component.ComponentAccessor
import java.sql.Timestamp
import com.atlassian.jira.event.type.EventDispatchOption

// Current user
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

def CAUSE = 15200 as Long
def dateToSet

// Get Managers
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def issueManager = ComponentAccessor.getIssueManager()

// Get the custom field
def customFieldCause = customFieldManager.getCustomFieldObject(CAUSE)

// Get the value of the custom field
def valueCause = customFieldCause.getValue(issue).toString()

// Logic
switch (valueCause) {
    case 'Development error':
        dateToSet = 30
        break
    case 'Regression':
        dateToSet = 60
        break
    case 'Special case':
        dateToSet = 90
        break
    default:
        dateToSet = 7
        break
}

// Calendar Instance with current date
Calendar dueDate = Calendar.getInstance()
dueDate.add(Calendar.DATE, dateToSet)

// Convert to a Timestamp instance
Timestamp dueDateTimestamp = new Timestamp(dueDate.getTimeInMillis())

// Setting up a new due date and updating the issue
issue.setDueDate(dueDateTimestamp)
issueManager.updateIssue(currentUser, issue, EventDispatchOption.ISSUE_UPDATED, false)
TAGS
AUG Leaders

Atlassian Community Events