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".
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
}
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)
}
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)
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)
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
}
}
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)
}
How to prevent more values from being selected in multi-select fields (Behaviours)
// 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()
}
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)
}
}
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)
}
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)
}
}
}
}
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)
}
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)
}
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
}
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')
}
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)
}
}
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())
}
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)
}
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
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)
}
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)
}
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
}
}
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)
}
}
}
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)
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)