Sync Parent from Child based on Updated Values

Lacey McDonnell May 14, 2016

Instead of having one line of post function to "copy field value to Parent" issue, I really, really REALLY want a Scripted Function that reads custom fields on a sub-task for change, and if changed carries that value over to the parent. I cannot figure out how to do this.

This would save us countless errors in accuracy (fields not being added to older workflows) not to mention hours of recreating our workflows when we merge our current instance into our goal instance.

Gratuitous Math: We have 20 workflows with about 10-15 steps each, and around 136 custom fields - we have an average of 55 applicable to each workflow step transition, of which there are an average of 4. 20*10=200 transitions. 55*(10*4)=2200 custom field lines. 200*2200=44,0000 lines to write back in for all transitions.

4 answers

2 votes
Ashraful Hasan [Adaptavist] May 16, 2016

Hi, The answer provided by @Jeremy Gaudet contains all the things you need to get the job done. The below script is possibly what you are looking for [thanks to Jeremy]. IT copies all custom fields change in sub task to the parent task:

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

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def issueManager = ComponentAccessor.getIssueManager()
def changeHistoryManager=  ComponentAccessor.getChangeHistoryManager()
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def issue = event.getIssue()

if(issue.getIssueType().isSubTask()){
    //get the parent issue
    def parentIssue = issue.parentObject
    //get all the custom fields
    def customFields = customFieldManager.getCustomFieldObjects(issue)

    //get the changed item list from change log
    def changeLogs = event.getChangeLog()
    def changedItem = changeHistoryManager.getAllChangeItems(issue).findAll { it.changeGroupId == changeLogs.id}

    //update parent issue if the changed item is a custom field
    changedItem.each { item ->
        if(customFields*.name.contains(item.field)) {
            def customFieldName = item.field
            def customField = customFieldManager.getCustomFieldObjectByName(customFieldName)
            def customFieldValue = issue.getCustomFieldValue(customField)

            //mutating parent issue
            MutableIssue mutableParentIssue = issueManager.getIssueObject(parentIssue.id)
            mutableParentIssue.setCustomFieldValue(customField, customFieldValue)
            //updating the parent issue
            issueManager.updateIssue(user, mutableParentIssue, EventDispatchOption.DO_NOT_DISPATCH, false);
        }
    }
}

  

Create a custom listener with "Issue Updated" event and test the script before deploying in production. 

 

Jeremy Gaudet
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 16, 2016

You probably don't want to do the issueManager.updateIssue() in the loop, as then you'll get one edit event/history per changed field.  It would be (slightly) better to track if there were any edits, and issue that outside of the loop if there were.

Either way, thanks for making it automatically pick up all custom field changes smile.

Lacey McDonnell May 16, 2016

What does that mean... "issueManager.updateIssue()" instead to track if there are edits?

As you can probably tell, I'm more app admin than sys admin smile

 

Jeremy Gaudet
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 16, 2016

I mean this line:

issueManager.updateIssue(user, mutableParentIssue, EventDispatchOption.DO_NOT_DISPATCH, false);

Should only be done once at the end, not multiple times in the middle.

Ashraful Hasan [Adaptavist] May 16, 2016

@Jeremy Gaudet , yep, as you point out the update statement should be outside loop, thanks smile

Lacey McDonnell May 17, 2016

As I am completely new to this, where does that line go? cheeky

Ashraful Hasan [Adaptavist] May 17, 2016

Here is the updated code as suggested. But please test the code before you use in production:

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

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def issueManager = ComponentAccessor.getIssueManager()
def changeHistoryManager=  ComponentAccessor.getChangeHistoryManager()
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def issue = event.getIssue()

if(issue.getIssueType().isSubTask()){
    //get the parent issue
    def parentIssue = issue.parentObject
    //get all the custom fields
    def customFields = customFieldManager.getCustomFieldObjects(issue)

    //get the changed item list from change log
    def changeLogs = event.getChangeLog()
    def changedItem = changeHistoryManager.getAllChangeItems(issue).findAll { it.changeGroupId == changeLogs.id}

    //mutating parent issue
    def mutableParentIssue = issueManager.getIssueObject(parentIssue.id)
    //update parent issue if the changed item is a custom field
    changedItem.each { item ->
        if(customFields*.name.contains(item.field)) {
            def customFieldName = item.field
            def customField = customFieldManager.getCustomFieldObjectByName(customFieldName)
            def customFieldValue = issue.getCustomFieldValue(customField)
            mutableParentIssue.setCustomFieldValue(customField, customFieldValue)
        }
    }
    //updating the parent issue only if condition is true
    if(mutableParentIssue.getModifiedFields()){
        issueManager.updateIssue(user, mutableParentIssue, EventDispatchOption.DO_NOT_DISPATCH, false);
    }
}

 

 

Lacey McDonnell May 17, 2016

Thanks so much! You all are the best smile 

Ratna Kumar Lekkala September 14, 2017

Hasan, Can you help me?

I need to update the parent Date/time field with current timestamp when a child task is done.

 

Need: Aging charts , like To track how long it took for testing to being after coding task is done, etc

 

The question is posted here. you can provide answer there.

https://community.atlassian.com/t5/Questions/How-to-update-a-parent-field-when-subtsk-state-changes/qaq-p/640019

KRC January 4, 2018

@Ashraful Hasan [Adaptavist]

I get this error on the following line

changedItem.each { item ->

expecting '}' found '-' I didn't change ur code, any thoughts? 

KRC January 4, 2018

@Ashraful Hasan [Adaptavist]

I created a post function which will copy parents summary and description to custom fields called parent summary and parent description on a subtask. It happens whenever a new subtask is created under the parent.

Now i have an ask that whenever summary or description is updated, it should update in both places, if updated in the parent, then update should reflect in subtask and vice versa.

Am trying to use your code, for error -&gt i useed-> and error got cleared, but am not sure if your code works in my case or not. trying to understand and troubleshoot.

1 vote
Jeremy Gaudet
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 16, 2016

You could add a Script Runner script listener to accomplish this.  Just listen for Issue Updated (or multiple additional events if the custom fields are present on the screens for the transitions that emit those events), and update the parent if any of the custom fields are present in the list of changed items.  I've done this in the opposite direction, but I have a fairly comprehensive example that updates the child's FixVersion (and a custom field called "Verified Version/s") if the child is changed to put it out of sync, or if the parent is changed.  It should be straightforward to extend it.  I have it listening on all projects, and the use the workflow to determine if it should fire, that way the listener config doesn't need to be modified when new projects are added.  Here it is, in case you find it helpful:

package com.myorg.listeners;

import org.apache.log4j.Category;
import com.atlassian.jira.ComponentManager;
import com.atlassian.jira.event.issue.AbstractIssueEventListener;
import com.atlassian.jira.event.issue.IssueEvent;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.project.version.Version;
import com.atlassian.jira.config.SubTaskManager;
import com.atlassian.jira.issue.IssueManager;
import com.atlassian.jira.issue.CustomFieldManager;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.event.type.EventDispatchOption;
import com.atlassian.jira.workflow.WorkflowManager;
import com.atlassian.jira.workflow.JiraWorkflow;
import java.util.ArrayList;
import java.util.Collection;

class SyncParentFixVerifiedVersionsToSubTask extends AbstractIssueEventListener 
{
    Category log = Category.getInstance("com.onresolve.jira.groovy");
    SubTaskManager subTaskManager = ComponentManager.getInstance().getSubTaskManager();
    WorkflowManager workflowManager = ComponentManager.getInstance().getWorkflowManager();
    IssueManager issueManager = ComponentManager.getInstance().getIssueManager();
    CustomFieldManager customFieldManager = ComponentManager.getInstance().getCustomFieldManager();

    Boolean changed = false;
    Long workflowId;

    @Override
    void workflowEvent(IssueEvent event) {
        try {
            if (subTaskManager.isSubTasksEnabled()) {
                Issue eventIssue = event.getIssue();
                JiraWorkflow workflow = workflowManager.getWorkflow(eventIssue);
                if (workflow.getName() == "Put Workflow Name Here") {
                    CustomField vvcf = customFieldManager.getCustomFieldObjectByName("Verified Version/s");
                    if ( !eventIssue.getIssueTypeObject().isSubTask() ) {
                        // Change is on a potential parent, sync to sub-tasks if there are any
                        Collection<Issue> subTasks = eventIssue.getSubTaskObjects();
                        List changeItems = event.getChangeLog().getRelated("ChildChangeItem");

                        // Sync FixVersion/s
                        if( changeItems.any {it.get('field')=='Fix Version'} ) {
                            changed = true;
                            // Collection<Version> fixVersions = new ArrayList<Version>();
                            // fixVersions = eventIssue.getFixVersions();
                            Collection<Version> fixVersions = eventIssue.getFixVersions();
                            if (!subTasks.isEmpty()) {
                                subTasks.each {
                                    it.setFixVersions(fixVersions);
                                }                     
                            }
                        }

                        // Sync Verified Version/s
                        if( changeItems.any {it.get('field')=='Verified Version/s'} ) {
                            changed = true;
                            Collection<Version> verifiedVersions = new ArrayList<Version>();
                            verifiedVersions = eventIssue.getCustomFieldValue(vvcf);
                            if (!subTasks.isEmpty()) {
                                subTasks.each {
                                    it.setCustomFieldValue(vvcf,verifiedVersions);
                                }
                            }
                        }

                        if (changed) {
                            subTasks.each {
                                issueManager.updateIssue(event.getUser(), it, EventDispatchOption.ISSUE_UPDATED, false);
                            }
                        }
                    } else {
                        // Change is on a sub-task, sync from the parent if they are out of sync
                        Issue parentIssue = eventIssue.getParentObject();

                        // Sync "Fix Version/s"
                        Collection<Version> parentFixVersions = parentIssue.getFixVersions();
                        if (parentFixVersions == null) parentFixVersions = new ArrayList<Version>();
                        Collection<Version> fixVersions = eventIssue.getFixVersions();
                        if (fixVersions == null) fixVersions = new ArrayList<Version>();
                        if(!(parentFixVersions.containsAll(fixVersions) && fixVersions.containsAll(parentFixVersions))) {
                            eventIssue.setFixVersions(parentFixVersions);
                            changed = true;
                        }

                        // Sync "Verified Version/s"
                        Collection<Version> parentVerifiedVersions = parentIssue.getCustomFieldValue(vvcf);
                        if (parentVerifiedVersions == null) parentVerifiedVersions = new ArrayList<Version>();
                        Collection<Version> verifiedVersions = eventIssue.getCustomFieldValue(vvcf);
                        if (verifiedVersions == null) verifiedVersions = new ArrayList<Version>();
                        if(!(parentVerifiedVersions.containsAll(verifiedVersions) && verifiedVersions.containsAll(parentVerifiedVersions))) {
                            eventIssue.setCustomFieldValue(vvcf,parentVerifiedVersions);
                            changed = true;
                        }
                        if (changed) {
                            issueManager.updateIssue(event.getUser(), eventIssue, EventDispatchOption.ISSUE_UPDATED, false);
                        }
                    }
                }
            }
        }
        catch (ex) {
            log.debug "Event: ${event.getEventTypeId()} fired for ${event.issue} and caught by SyncParentFixVerifiedVersionsToSubTask"
            log.debug (ex.getMessage())
        }
    }
}

The main difference would be updating the parent instead of the child, but the second block covers getting the parentIssue, and so this should contain examples of everything you need.

Jeremy Gaudet
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 16, 2016
Lacey McDonnell May 16, 2016

Based on "It should be straightforward to extend it." I want to make sure I am reading this correctly... So this would need to be written/extended for each custom field we have, though, right? My point here is that we have ~136 custom fields. It'd be a really long script..! sad 

 

0 votes
Phophi Kgotsane August 25, 2017

Hi All,

I am battling with writing a script listener that will update the parent issue's comment with the sub tasks comments whenever a sub task is added. I currently have the following script:

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

def commentManager = ComponentAccessor.getCommentManager()

def issueManager = ComponentAccessor.getIssueManager()
def commentManger = ComponentAccessor.getCommentManager()
def changeHistoryManager= ComponentAccessor.getChangeHistoryManager()
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def issue = event.getIssue()

if(issue.getIssueType().isSubTask()){
//get the parent issue
def parentIssue = issue.parentObject
//get last comment
def comments = commentManager.getLastComment(issue)

//get the changed item list from change log
def changeLogs = event.getChangeLog()
def changedItem = changeHistoryManager.getAllChangeItems(issue).findAll { it.changeGroupId == changeLogs.id}

//mutating parent issue
def mutableParentIssue = issueManager.getIssueObject(parentIssue.id)

//mutating parent comment
def mutableParentComment = commentManger.getMutableComment(Long commentId)

//updating the parent issue only if condition is true
if(mutableParentIssue.getModifiedFields()){
issueManager.updateIssue(user, mutableParentIssue, EventDispatchOption.DO_NOT_DISPATCH, false);
}
}

0 votes
JamieA
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 15, 2016

Just to clarify, you want to copy all custom fields on subtasks upwards to its parent (where the CF is associated with both parent and child), when any CF on the subtask changes ?

Is this really useful? What happens when an issue has multiple subtasks, if the CF is changed on one, it will have the same value as its parent, but not the same value as its sibling subtasks.

If the subtask CFs should be the same as the parent CFs, isn't it easier to only put the CFs on the parent issue? Script fields or web panels can make the values show on the child issue if that's the desire.

Lacey McDonnell May 16, 2016

Jamie:

By design, we only ever have one sub-task per parent issue. This is due to integration with a customer portal program that we have. We work on the sub-task issues, not the parent - so to have the CFs only on the parent level isn't really the most convenient/effective/efficient for our use.

So in essence, yes, all custom fields on sub-tasks upwards to its parent when any CF on the sub-task changes.

I did find a plugin for this but it seems to be only contingent on workflow steps which isn't ideal.

Suggest an answer

Log in or Sign up to answer