Snippets

Matthew Clark (Adaptavist) Script Listener (ISSUE UPDATED ONLY) - Push Custom Field Value updated to Parent-Child Linked issues. (Not recursive)

Created by Matthew Clark last modified

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.changehistory.ChangeHistoryManager
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.history.ChangeItemBean
import com.atlassian.jira.issue.link.IssueLink
import com.atlassian.jira.issue.link.IssueLinkManager
import com.atlassian.jira.issue.link.IssueLinkTypeManager
import com.atlassian.jira.user.ApplicationUser
import org.apache.log4j.Logger
import org.apache.log4j.Level
import java.text.SimpleDateFormat

def log = Logger.getLogger(getClass())
log.setLevel(Level.DEBUG)

/**
 *
 *
 *   Use as a listener with the "ISSUE UPDATED" event and this currently copies a standard Text field value.
 *
 * In portfolio we have a parent link and also the concept of child issues
 *
 * If we want to sync field values between the parent and child when an issue is updated we can use a listener that
 * triggers on the issue updated event and performs several other checks.
 *
 * 1 - Check if the issue that was updated has portfolio links to either a parent or a child
 * 2 - Check that the field we want to sync the value for was updated on this issue update event
 * 3 - Find the relevant link types to either the parent or the child and then push the updated field value to the linked issue
 * 4 - Make sure we only update the issues linked under a given link type which by default for Portfolio is Parent-Child Link
 *
 * The link details used by portfolio parent - child issues as shown in database on my test server
 *  SQL if you need to look for yourself: SELECT * FROM issuelinktype
 *
 * [ID:10201, LINKNAME:Parent-Child Link, INWARD:is child of, OUTWARD:is parent of, PSTYLE:jira_jpos_parent_child]
 *
 * Limitation Note: 
 *    I have coded this to push updates from parent to children and also when a child is updated it will push the update to the parent.
 *    HOWEVER when child updates the parent this script will NOT then push the update to all the other children under the parent
 *    because I do not fire the issue updated event when the parent is altered by this script.
 *
 *    It might be posibble to just fire the issue updated event when updating the parent so that then triggers this same script to
 *    then update all childen under that parent but I dont want to do this here as that could cause loops and crash Jira.
 *
 *    Feel free to try this yourselves but I will not do this in this script as I do not want to break peoples Jira installs
 *
 */

CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
Issue issue = event.issue as Issue
CustomField myField = customFieldManager.getCustomFieldObject("customfield_10013")

if(! myField){
    log.debug("The Field you are looking for does not exist, check the id")
    return
}

Boolean hasFieldChanged = checkFieldHasBeenChanged(issue,myField)

if(hasFieldChanged){

    log.debug("Field Change Detected so Will copy to Portfolio Child Or Parent if the link is found")

    pushUpdateToPortfolioLinkedIssues(issue,myField)
}


/**
 *
 * @param issue - Issue that triggered Listener
 * @param customField - customField object we want to check if it was changed on this update
 * @return boolean true if the field value was updated
 *
 * Notes: this method could probably be improved. I get the Timestamps for the issue that was updated
 *        and the create time for the change history item of the field change. Then convert them to
 *        the time format "dd-M-yyyy hh:mm:ss" to make it so it only goes to the second instead of the
 *        micro second as there is a chance the times will be different in micro seconds but should be
 *        the same second. If you have a slower system that then you may need to change the format so
 *        it only goes to the minute.
 *         Or implement a time interval difference that is acceptable and allow for that in your Timestamp
 *        comparisons
 *
 */
Boolean checkFieldHasBeenChanged(Issue issue, CustomField customField){

    def log = Logger.getLogger(getClass())
    log.setLevel(Level.DEBUG)

    ChangeHistoryManager changeHistoryManager = ComponentAccessor.getChangeHistoryManager()

    String fieldName = customField.getName()
    log.debug("FieldName = " + fieldName)

    ChangeItemBean changeItem = changeHistoryManager.getChangeItemsForField(issue,fieldName).last()

    // we will compare change issue update and field update times to the second, change the format if you want it to be to the minute
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd-M-yyyy hh:mm:ss")

    String issueUpdateTime = simpleDateFormat.format(issue.updated)
    String fieldUpdateTime = simpleDateFormat.format(changeItem.created)
    //defualt value is timestamp like 2019-09-14 20:18:16.823 and the value after the . can differ very slightly for the time the
    //issue was updated versus the time the changehistory was updated.

    log.debug("""
             issueUpdatedTime = $issueUpdateTime
             fieldUpdateTime  = $fieldUpdateTime
             Comparison Logic check:      ${issueUpdateTime == fieldUpdateTime}""")

    // if the update time of the field and the issue are to the same second then we assume the field was changed
    // i do not check the actualy values changed. You would need to get the last change item and the one before to compare this
    return issueUpdateTime == fieldUpdateTime

}


/**
 *
 * @param issue - Issue that triggered Listener
 * @param fieldToCopy - CustomField Object for the field you want to update
 */
void pushUpdateToPortfolioLinkedIssues(Issue issue, CustomField fieldToCopy){

    def log = Logger.getLogger(getClass())
    log.setLevel(Level.DEBUG)

    String linkTypeName     = "Parent-Child Link"

    def currentIssuesFieldValue = issue.getCustomFieldValue(fieldToCopy)

    if(currentIssuesFieldValue) {

        IssueLinkTypeManager issueLinkTypeManager = ComponentAccessor.getComponent(IssueLinkTypeManager)
        IssueLinkManager issueLinkManager = ComponentAccessor.getIssueLinkManager()
        IssueManager issueManager = ComponentAccessor.getIssueManager()
        ApplicationUser user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()

        def linkTypes = issueLinkTypeManager.getIssueLinkTypes(false)
        Long parentChildPortfolioLinkID = linkTypes.find { linkType ->
            linkType.name == linkTypeName
        }?.id

        Collection<IssueLink> parentToChildLinks = issueLinkManager.getInwardLinks(issue.id)
        Collection<IssueLink> childToParentLinks = issueLinkManager.getOutwardLinks(issue.id)

        //Process Children issues and push field update to child issues
        parentToChildLinks.each { linkObject ->

            if (linkObject.linkTypeId == parentChildPortfolioLinkID) {

                def childIssue = linkObject.sourceObject as MutableIssue
                if (childIssue) {
                    log.debug("Dest Issue Key = ${childIssue.key}")

                    def destIssueCurrentFieldValue = childIssue.getCustomFieldValue(fieldToCopy)

                        ////detect if destination issue has value already, if the values are already the same, do nothing
                        if (currentIssuesFieldValue == destIssueCurrentFieldValue) {
                            log.debug("Values already the same")
                        } else {
                            childIssue.setCustomFieldValue(fieldToCopy, currentIssuesFieldValue)
                            issueManager.updateIssue(user, childIssue, EventDispatchOption.DO_NOT_DISPATCH, false)
                        }

                }

            }
        }

        //Process Parent Issues and push field update to parent issues
        childToParentLinks.each { linkObject ->

            if (linkObject.linkTypeId == parentChildPortfolioLinkID) {


                def parentIssue = linkObject.destinationObject as MutableIssue
                if (parentIssue) {

                    log.debug("Dest Issue Key = ${parentIssue.key}")

                    def destIssueCurrentFieldValue = parentIssue.getCustomFieldValue(fieldToCopy)

                        ////detect if destination issue has value already, if the values are already the same, do nothing
                        if (currentIssuesFieldValue == destIssueCurrentFieldValue) {
                            log.debug("Values already the same")
                        } else {
                            parentIssue.setCustomFieldValue(fieldToCopy, currentIssuesFieldValue)
                            issueManager.updateIssue(user, parentIssue, EventDispatchOption.DO_NOT_DISPATCH, false)
                        }

                }

            }
        }

    }else {
        log.debug("The current Issue ${issue.key} does not have a value in the chosen custom field ${fieldToCopy.id}")
    }


}

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.