Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

You might find Smart Checklist integration with ScripRunner useful for updating checklists using automated scripts. 

Note

Since we use Issue Properties as primary memory storage for checklists, it's required to update Issue Property to have checklists updated with any type of automation. Updating the "Checklists" Custom Field won't bring the desired result. We are in touch with ScriptRunner support to identify if it's possible to do via scripting in the current implementation.

Table of Contents
maxLevel2

...

Table of Contents
minLevel1
maxLevel2
outlinefalse
styledefault
typelist
printabletrue
Panel
bgColor#DEEBFF

☝🏼 NOTE: We recommend running ScriptRunner scripts only for issues without multiple checklist tabs, as they may not work correctly when tabs are present.

Add checklist to an issue

However, if the issue has no checklist set yet - it's possible to initiate initial rendering by pushing adding value to the custom field.

...

Code Block
languagegroovy
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.event.type.EventDispatchOption

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def issueManager = ComponentAccessor.getIssueManager()

def issue = issueManager.getIssueObject("PROJ-7")
def changeHolder = new DefaultIssueChangeHolder()
def String myval = "- ToDo List\n+ Checked\nx Skipped\n~ In Progress\n# Another ToDo List\n- Unchecked\n> Quote line 1 https://rw.rw\\n> Quote line 2\n> Quote line 3\n"

def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser();
// a text field
def textCf = customFieldManager.getCustomFieldObjectByName("Checklists")
issue.setCustomFieldValue(textCf.updateValue(null, myval)
issueManager.updateIssue(user, issue, new ModifiedValue(issue.getCustomFieldValue(textCf), myval),changeHolder)
EventDispatchOption.ISSUE_UPDATED, false)
Result

...


Update/append checklist with post function during a workflow status change

...

  1. Go to Workflow Editor

  2. Choose "Script Post Function" Script Runner

  3. Choose "Custom Script Post Function"

  4. Add inline Script

  5. Variables checklistProd and checklistStage used for keeping the proper value of the checklist (in text format)

    ScriptRunner

    Code Block
    languagegroovy
    import com.atlassian.jira.component.ComponentAccessor
    import com.atlassian.jira.issue.ModifiedValue
    import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
    import org.apache.log4j.Level
    
    log.setLevel(Level.DEBUG)
    
    //Grab necessary Components
    def cfm = ComponentAccessor.getCustomFieldManager()
    def optionsManager = ComponentAccessor.getOptionsManager()
    
    def cfEnv = cfm.getCustomFieldObjectByName("Environment")
    def cfEnvtValue = issue.getCustomFieldValue(cfEnv)
    def cfEnvValueString = cfEnvtValue.toString()
    def changeHolder = new DefaultIssueChangeHolder()
    
    def checklistProd = "- ToDo List\n+ Checked\nx Skipped\n~ In Progress\n# Another ToDo List\n- Unchecked\n> Quote line 1 https://rw.rw\\n> Quote line 2\n> Quote line 3\n"
    def checklistStage = "- Uno\n+ Dos\nx Tres\n~ QuatroCuatro\n"
    
    
    log.info("checklistProd: " + checklistProd)
    log.info("checklistStage: " + checklistStage)
    log.info("Environment"+ cfEnvValueString)
    
    
    if (cfEnvValueString == "Production") {
    	//Set custom text field
    	def cfClient = cfm.getCustomFieldObjectByName("Checklists")
    	issue.setCustomFieldValue(cfClient,checklistProd)
    } else if (cfEnvValueString == "Staging") {
    	//Set custom text field
    	def cfClient = cfm.getCustomFieldObjectByName("Checklists")
    	issue.setCustomFieldValue(cfClient,checklistStage)
    }
    
    


  6. Save the script. IMPORTANT! Make the Post Function is placed as a #1 Step!

...

Here is an example of the implementation flow:

...

Code Block
import com.atlassian.jira.ComponentManager

import com.atlassian.jira.ComponentManager

import com.atlassian.jira.security.roles.ProjectRoleManager

import com.atlassian.jira.component.ComponentAccessor

ProjectRoleManager projectRoleManager = ComponentManager.getComponentInstanceOfType(ProjectRoleManager.class);

def usersRoles = projectRoleManager.getProjectRoles(ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser(), issue.getProjectObject())*.name;

return usersRoles.contains("Administrators") com.atlassian.jira.security.roles.ProjectRoleManager

import com.atlassian.jira.component.ComponentAccessor

ProjectRoleManager projectRoleManager = ComponentManager.getComponentInstanceOfType(ProjectRoleManager.class);

def usersRoles = projectRoleManager.getProjectRoles(ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser(), issue.getProjectObject())*.name;

return usersRoles.contains("Administrators")

Validate the completion of specific checklist items before transition

Let’s now see more advanced case for validating a transition depending on specific items checked in checklist, and then automatically change issue status when uncheck specific item [DEMO](https://drive.google.com/file/d/1lhxzj6W5LLkrxANLaKcutqcZO5KyytEd/view?usp=sharing ).

Implementation flow:

This is implemented in two steps, first one for validating before transition to new status. Second steps is meant to change status back to “TODO” if the user uncheck the item of checklist.

Transition validator

  1. Go to Workflow Editor

    Screenshot 2024-06-11 at 19.23.00.pngImage Added
  2. Select Simple scripted validator [ScriptRuner]

...

  1. Add Condition (script code)

Code Block
import org.apache.log4j.Logger

// Set up logging
def log = Logger.getLogger("com.atlassian.jira.customfield")
log.setLevel(org.apache.log4j.Level.DEBUG)

// Get the custom field value
def checklistCF = cfValues["Smart Checklist"]

if (!checklistCF) {
    log.debug("Custom field value is empty or null")
    return true
}

def checklists = checklistCF._items
// Iterate through checklist items
for (item in checklists[0]) {
    log.debug("Validating item: ${item.value} - ${item.status.statusState}")
    if (item.value.equals("RTL Fix") || item.value.contains("Quality Check")) {
          if (!item.status.statusState.toString().equals("CHECKED")) {    
            log.debug("Item '${item.value}' has status '${item.status.statusState}' which is not checked.")
            // No need to continue checking, we already found one item with status not checked
            return false
        }
    }
}
log.debug("All required checklist items are marked as completed or not present")
return true
  1. Select Field and Error Message

...

  1. Publish changes.

Now, the transition to “done” is not possible anymore without having both “RTL Fix” and “Quality Check” checked.

Change issue status when unchecking specific items

  1. From ScriptRunner admin page, select Listeners tab and press Create Listener button

    Screenshot 2024-06-11 at 19.32.40.pngImage Added
  2. Under script section add following

Code Block
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.event.issue.IssueEvent
import org.apache.log4j.Logger
import com.atlassian.jira.workflow.TransitionOptions
import com.atlassian.jira.bc.issue.IssueService
import com.atlassian.jira.issue.IssueInputParameters
import com.atlassian.jira.workflow.WorkflowTransitionUtil
import com.atlassian.jira.issue.IssueInputParametersImpl

// Initialize log4j logger
def log = Logger.getLogger("com.acme.LogCustomFields")
log.setLevel(org.apache.log4j.Level.DEBUG)
CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
IssueEvent issueEvent = event as IssueEvent
Issue issue = issueEvent.getIssue()
def changeLog = issueEvent.getChangeLog()
def changeItems = changeLog.getRelated("ChildChangeItem")

// Function to transition issue to "TO DO"
def transitionIssueToDo(Issue issue, Logger log) {
    def issueService = ComponentAccessor.getIssueService()
    def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
    def transitionName = "To Do" ////  --- SET HERE STATUS TO BE TRANSITIONED TO IF UNCHECK ITEM
    def actionId = null

    def workflow = ComponentAccessor.getWorkflowManager().getWorkflow(issue)
    def actions = workflow.getAllActions()
    actions.each { action ->
        if (action.name == transitionName) {
            actionId = action.id
        }
    }

    if (actionId) {
        def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

        def transitionValidationResult = issueService.validateTransition(currentUser, issue.id, actionId, new IssueInputParametersImpl())

        if (transitionValidationResult.isValid()) {
            transitionResult = issueService.transition(currentUser, transitionValidationResult)
            if (transitionResult.isValid()) { 
                log.debug("Transitioned issue $issue through action $actionId") 
            } else { 
                log.debug("Transition result is not valid  Details: ${transitionResult.errorCollection}") 
            }
        } else {
            log.debug("The transitionValidation is not valid")
        }
    } else {
        log.error("Transition to 'To Do' not found")
    }
}

// Check if any of the custom fields were changed
changeItems.each { changeItem ->
    def fieldName = changeItem.getString("field")
    if(fieldName.equals("Checklists")){ // DONOT CHANGE 'Checklists'
        def theCustomFieldObject = "Smart Checklist" // DONOT CHANGE "Smart Checklist"
        def customFieldList = customFieldManager.getCustomFieldObjectsByName(theCustomFieldObject)
        if (customFieldList && !customFieldList.isEmpty()) {
            def customField = customFieldList.first()
            def fieldValue = issue.getCustomFieldValue(customField)

            def items = fieldValue[0]._items

            for(item in items){
                log.debug("Validating item: ${item.value} - ${item.status.statusState}")
                if (item.value.equals("RTL Fix") || item.value.contains("Quality Check")) { // REPLACE WITH SPECIFIC ITEMs
                    if (!item.status.statusState.toString().equals("CHECKED")) {    
                        log.debug("Item '${item.value}' has status '${item.status.statusState}' which is not checked.")
                        transitionIssueToDo(issue, log)
                        break
                    }
                }    
            }
        } else {
            log.debug("Custom field with name $theCustomFieldObject not found")
        }
   }
}
  1. Select Projects to be applied on/Global and Issue Updated as Events

...

  1. Update listener and you are ready to use it.

Create sub-task automatically on all applied checklist items to the issue

This use case allows you to automatically create sub-tasks for each newly added checklist item in an issue. As 'To-Do' items are applied to the issue, sub-tasks will be created for each of them automatically, without any additional manual effort.

Implementation flow:

  1. Go to the Jira Administration → Manage apps > SriptRunner

  2. Click on Listeners tab

  3. Click Create Listener

    image-20240909-084852.pngImage Added

  4. Select Custom listener

  5. Fill in the:

    1. Nam - add name to your listener.

    2. Applied to option - Global or only selected projects.

    3. Events - Issue updated.

    4. Copy Script below.

  6. Click Update and you are ready to use it.

...

Script:

Code Block
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.bc.issue.IssueService
import com.atlassian.jira.bc.issue.IssueService.CreateValidationResult
import com.atlassian.jira.bc.issue.IssueService.IssueResult
import com.atlassian.jira.issue.IssueInputParameters
import com.atlassian.jira.issue.IssueInputParametersImpl
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.issuetype.IssueType
import org.apache.log4j.Logger
import com.atlassian.jira.issue.link.IssueLinkManager
import com.atlassian.jira.issue.link.IssueLinkType
import com.atlassian.jira.issue.link.IssueLink
import com.atlassian.jira.issue.link.IssueLinkTypeManager


// Initialize log4j logger
def log = Logger.getLogger("com.acme.LogCustomFields")
log.setLevel(org.apache.log4j.Level.DEBUG)

CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
IssueEvent issueEvent = event as IssueEvent
Issue issue = issueEvent.getIssue()
def changeLog = issueEvent.getChangeLog()
def changeItems = changeLog.getRelated("ChildChangeItem")

// IssueService for creating sub-tasks
IssueService issueService = ComponentAccessor.getIssueService()
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Get all sub-tasks of the current issue
def subTasks = issue.getSubTaskObjects().collect { it.getSummary() }


// Check if any of the custom fields were changed
changeItems.each { changeItem ->
    def fieldName = changeItem.getString("field")
    
    if (fieldName.equals("Checklists")) {  // DONOT CHANGE 'Checklists'
        def theCustomFieldObject = "Smart Checklist"  // DONOT CHANGE "Smart Checklist"
        def customFieldList = customFieldManager.getCustomFieldObjectsByName(theCustomFieldObject)
        
        if (customFieldList && !customFieldList.isEmpty()) {
            def customField = customFieldList.first()
            def fieldValue = issue.getCustomFieldValue(customField)
            def items = fieldValue[0]._items  // The list of checklist items

            for (item in items) {
                def itemValue = item.value
                log.debug("Validating item: ${itemValue} ")

                // Create sub-task only if it's a new item (not in existing sub-tasks)
                if (!subTasks.contains(itemValue)) {
                    log.debug("Creating sub-task for new item: ${itemValue}")

                    def issueTypeManager = ComponentAccessor.getConstantsManager().getAllIssueTypeObjects()
                        
                        // Look for the issue type named "Sub-task"
                    IssueType subTaskIssueType = issueTypeManager.find { it.isSubTask() }
                    

                    log.debug("Issue type ${subTaskIssueType} ");
                        
                    IssueInputParameters issueInputParameters = issueService.newIssueInputParameters()
                        issueInputParameters.setSummary(itemValue)
                            .setIssueTypeId(subTaskIssueType.id)  // Retrieve the Sub-task issue type ID
                            .setReporterId(user.getName())  // Set the reporter to the user who triggered the action
                            .setProjectId(issue.getProjectId())
                            .setAssigneeId(issue.getAssigneeId())

                        // Validate sub-task creation
                        IssueService.CreateValidationResult validationResult = issueService.validateSubTaskCreate(user, issue.getId(), issueInputParameters)
                        
                        if (validationResult.isValid()) {
                            IssueService.IssueResult createResult = issueService.create(user, validationResult)
                            

                            
                            if (createResult.isValid()) {
                                def subTask = createResult.issue

                                
                                IssueLinkManager issueLinkManager = ComponentAccessor.getIssueLinkManager()
                                Collection<IssueLink> issueLinks = issueLinkManager.getIssueLinks(issue.getId())
                                int maxSequence = 0

                                // Iterate through the collection to find the maximum sequence
                                issueLinks.each { issueLink ->
                                    // Get the sequence for the current issue link
                                    int sequence = issueLink.getSequence() ?: 0

                                    // Update the max sequence if the current one is greater
                                    if (sequence > maxSequence) {
                                        maxSequence = sequence
                                    }
                                }

                                // Output the maximum sequence
                                log.debug("The maximum sequence value is: ${maxSequence}")

                                // You can also use maxSequence + 1 if needed
                                int nextSequence = maxSequence + 1
                                def issueLinkTypeManager = ComponentAccessor.getComponent(com.atlassian.jira.issue.link.IssueLinkTypeManager)
                                // Get all issue link types
                                Collection<IssueLinkType> issueLinkTypes = issueLinkTypeManager.getIssueLinkTypes(false)
                                def matchingLinkType = issueLinkTypes.find { it.name == "jira_subtask_link" }
                                issueLinkManager.createIssueLink(issue.getId(), subTask.id, matchingLinkType.id, nextSequence, user)
                            } else {
                                log.error("Failed to create sub-task: ${createResult.errorCollection}")
                            }
                        } else {
                            log.error("Sub-task validation failed: ${validationResult.errorCollection}")
                        }
                } else {
                    log.debug("Sub-task for item: ${itemValue} already exists, skipping creation")
                }
            }
        } else {
            log.debug("Custom field with name $theCustomFieldObject not found")
        }
    }
}

Now every time new checklist item is added or template is applied to the issue, the corresponding sub-task will be automatically created!

Set items in Done when issue is Done

This use case allows you to automatically mark all checklist items to done when issue is resolved or status is changed to done.

Implementation flow:

  1. Navigate to Workflow:

    • Go to Jira Settings (⚙️ icon) → IssuesWorkflows.

    • Find the workflow associated with the project and issue type where you want the script to run.

    • Click Edit on the desired workflow.

  2. Edit the Workflow Transition:

    • Identify the transition that moves the issue to "Done" or "Resolved" (usually named "Resolve Issue" or "Done").

    • Click on the transition arrow leading into the "Done" or "Resolved" status.

  3. Add a Post Function:

    • In the transition's Post Functions tab, click Add Post Function.

    • Choose "Custom Script Post Function" (provided by ScriptRunner).

  4. Write the Script:

Code Block
import com.atlassian.jira.component.ComponentAccessor
import groovyx.net.http.RESTClient
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.CustomFieldManager


def username = 'username'  // Replace with actual username or read value from system environments
def password = 'password'    // Replace with actual password or read value from system environments
def apiUrl = 'jira instance url' // Replace with your domain
def markAsDownPath = '/rest/railsware/1.internal/checklist/markAsDone' // might be needed to add /jira at the begining
def customFieldName = "Smart Checklist"

if (!issue) {
    log.error("Issue not found in transient variables.")
    return "Issue not found"
}

log.info("Issue details: ${issue.key}, Status: ${issue.status.name}")

def doneStatus = ComponentAccessor.constantsManager.getStatusByName("Done")
def resolvedStatus = ComponentAccessor.constantsManager.getStatusByName("Resolved")

if (issue.status.id == doneStatus.id || issue.status.id == resolvedStatus.id) {
    log.debug("Issue ${issue.key} is now in ${issue.status.name} status. Updating checklist items.")
    try {
        CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
        CustomField customField = customFieldManager.getCustomFieldObjectByName(customFieldName)
        
        def authString = "${username}:${password}".bytes.encodeBase64().toString()

        if (!customField) {
            log.error("Custom field '${customFieldName}' not found.")
            return "Custom field not found"
        }

        def checklists = issue.getCustomFieldValue(customField)

        if (!checklists || checklists.isEmpty()) {
            log.warn("No checklists found for issue ${issue.key}.")
            return "No checklists found"
        }

        def restClient = new RESTClient(apiUrl)
        checklists.forEach { checklist ->
            log.info("Fetching: ${apiUrl}/jira/rest/railsware/1.internal/checklist/markAsDone?checklistId=${checklist.id}")


            def response = restClient.put(
                path: markAsDownPath,
                query: [checklistId: checklist.id],
                contentType: 'application/json',
                headers: [Authorization: "Basic ${authString}"]

            )

            if (response.status > 200 && response.status < 300 ) {
                log.info("Successfully marked checklist ID ${checklist.id} as done.")
            } else {
                log.error("Failed to mark checklist ID ${checklist.id}. Response: ${response.status} - ${response.data}")
            }
        }
    } catch (Exception e) {
        log.error("Exception occurred while updating checklists: ${e.message}", e)
    }
} else {
    log.info("Issue ${issue.key} is not in Done or Resolved status. No action taken.")
}
  1. Go to “Post Functions”

    • make sure that custom script is the last in the post-functions functions

...

  1. Publish workflow and test

Make items mandatory based on the issue status

This use case allows you to automatically mark specific checklist items as mandatory when issue is transitioned to the specific issue status.

Implementation flow:

  1. Navigate to Workflow:

    • Go to Jira Settings (⚙️ icon) → IssuesWorkflows.

    • Find the workflow associated with the project and issue type where you want the script to run.

    • Click Edit on the desired workflow.

  2. Edit the Workflow Transition:

    • Select the issue status you want to add a Post Function to

  3. Add a Post Function:

    • In the transition's Post Functions tab, click Add Post Function.

    • Choose "Custom Script Post Function" (provided by ScriptRunner).

  4. Add the Script:

Code Block
languagegroovy
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.MutableIssue
import org.apache.log4j.Logger
import groovyx.net.http.RESTClient
import groovy.json.JsonOutput

// Initialize log4j logger
def log = Logger.getLogger("com.acme.UpdateChecklistItem")
log.setLevel(org.apache.log4j.Level.DEBUG)

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def issueManager = ComponentAccessor.getIssueManager()
def issue = transientVars.get("issue") as MutableIssue //USE THIS FOR POSTFUNCTIONS

def authString = "Personal Access Token" // Add Personal Access Token
def apiUrl = 'Jira_base_url' // Replace with your domain
def updateChecklistPath = 'rest/railsware/1.internal/checklist' // might be needed to add /jira at the begining
def requiredValues = ["Task A", "Task B", "Task C"]   // Here are the values you want to mark as mandatory


def CUSTOM_FIELD_NAME = "Smart Checklist" // DONOT CHANGE THIS

CustomField customField = customFieldManager.getCustomFieldObjectsByName(CUSTOM_FIELD_NAME)?.first()

if (!customField) {
    log.debug("Custom field '$CUSTOM_FIELD_NAME' not found")
    return
}


def currentChecklist = issue.getCustomFieldValue(customField)

if(!currentChecklist){
    log.debug("No checklist found for issue")
    return;
}

def restClient = new RESTClient(apiUrl)

for (checklist in currentChecklist) {
    def items = checklist._items 

    def matchingItemIds = items.findAll { requiredValues.contains(it.value) }*.id
    
    if (matchingItemIds) {
        def requestBody = matchingItemIds.collect { id ->
            [
                id   : id,
                mandatory : true
            ]
        }


        def jsonPayload = JsonOutput.toJson(requestBody)


        log.debug("URL: ${apiUrl}/${updateChecklistPath}/${checklist.id}")
        log.debug("body: ${jsonPayload}")
        try {
            def response = restClient.put(
                path: "${apiUrl}/${updateChecklistPath}/${checklist.id}",
                body: jsonPayload,
                requestContentType: 'application/json',
                headers: [Authorization: "Bearer ${authString}"]
            )

            log.debug("Response Code: ${response.status}")
            log.debug("Response: ${response.data}")

        } catch (Exception e) {
            log.error("Failed to update checklist items: ${e.message}")
        }

        log.debug("Checklist custom field updated successfully!")
    } else {
        log.debug("No checklist items matched the filter condition.")
    }
}
Panel
bgColor#DEEBFF

💡HINT: To get the Personal Access Token navigate to User icon → Profile → Personal Access Tokens → Create Token

image-20240212-154345.pngImage Added
  1. Publish workflow and test

Insert excerpt
Get Started
Get Started
namesupport-email (checklist-server)
nopaneltrue