JIRA workflows, advance parent, clone and reverse link

The JIRA issue tracking system is very powerful. It supports customizable work-flows which can be tuned to allow you to have better control over the issue life-cycle.

When you need additional functions in a work-flow, the Groovy script runner plug-in gives you many useful extra checks and post-functions or allows you to easily add more.

In my case, I needed to tweak some of the provided post-functions.

Transition parent

On a sub-task, it can be useful to force transitions on the parent issue. For example to assure that the parent is marked as “in progress” when progress is started on a sub-task.

Put the following code in $JIRA_INSTALLATION_DIR/atlassian-jira/WEB-INF/classes/com/onresolve/jira/groovy/canned/workflow/postfunctions/TransitionParent.groovy:

package com.onresolve.jira.groovy.canned.workflow.postfunctions
 
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.config.ConstantsManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.util.ErrorCollection
import com.atlassian.jira.workflow.JiraWorkflow
import com.onresolve.jira.groovy.canned.CannedScript
import com.onresolve.jira.groovy.canned.utils.CannedScriptUtils
import com.onresolve.jira.groovy.canned.utils.WorkflowUtils
import com.opensymphony.workflow.loader.ActionDescriptor
import com.opensymphony.workflow.loader.StepDescriptor
import org.apache.log4j.Category
import com.atlassian.crowd.embedded.api.User
 
class TransitionParent implements CannedScript {
 
    ComponentManager componentManager = ComponentManager.getInstance()
    Category log = Category.getInstance(ResolveParentAfterSubtasks.class)
    def projectManager = componentManager.getProjectManager()
    public final static String FIELD_PARENTACTION = "FIELD_PARENTACTION"
    public final static String FIELD_RESOLUTION_ID = "FIELD_RESOLUTION_ID"
 
    String getName() {
        return "Transition parent"
    }
 
    String getDescription() {
        return """This will do the given action on the parent<br>
        """
    }
 
    List getCategories() {
        ["Function"]
    }
 
 
    Integer getActionId(Issue issue, String actionName) {
        JiraWorkflow workflow = componentManager.getWorkflowManager().getWorkflow(issue)
        StepDescriptor step = workflow.getLinkedStep(issue.status)
        ActionDescriptor ad = step.getActions().find {it.name == actionName} as ActionDescriptor
        ad?.id
    }
 
    List getParameters(Map params) {
        [
            [
                Name:FIELD_PARENTACTION,
                Label:"Parent action",
                Description:"Choose the action to do on the parent when the sub-tasks are resolved",
                Type: "list",
                Values: CannedScriptUtils.getAllWorkflowActions(false),
            ],
            [
                Label:"Resolution",
                Name:FIELD_RESOLUTION_ID,
                Type: "list",
                Description:"Resolution to use on the parent",
                Values: CannedScriptUtils.getResolutionOptions(true),
            ],
            // todo: need to allow a sub-task resolution
        ]
 
    }
 
    public ErrorCollection doValidate(Map params, boolean forPreview) {
        // todo: check this is on a sub-task type
        null
    }
 
    Map doScript(Map params) {
        log.debug ("TestCondition.doScript with params: ${params}");
 
        String actionName = params[FIELD_PARENTACTION] as String
        MutableIssue subtask = params['issue'] as MutableIssue
        User user = WorkflowUtils.getUser(params)
        String resolutionId = params[FIELD_RESOLUTION_ID] as String
 
        log.debug ("actionName: $actionName")
        log.debug ("subtask: $subtask")
        log.debug ("subtask.isSubTask(): ${subtask.isSubTask()}")
 
        // if this action is resolve and all sub-tasks are resolved
        if (subtask.isSubTask()) {
            MutableIssue parent = subtask.getParentObject() as MutableIssue
 
            log.debug ("Resolve parent")
            Integer actionId = actionName?.replaceAll(/ .*/, "") as Integer
            if (WorkflowUtils.hasAction(parent, actionId)) {
                WorkflowUtils.resolveIssue(parent, actionId, user, resolutionId, [:])
            } else {
               log.warn("Action name: $actionName not found for this step.")
            }
        }
 
        return params
    }
 
    String getDescription(Map params, boolean forPreview) {
        ConstantsManager constantsManager = ComponentManager.getInstance().getConstantsManager()
 
        StringBuffer sb = new StringBuffer()
        sb << getName() + "<br>Transition " + params[FIELD_PARENTACTION]
        sb.toString()
    }
 
    public Boolean isFinalParamsPage(Map params) {
        true
    }
}

The script has been submitted for inclusion in future versions of the plug-in, see GRV-123.

Clone and link with reverse link

The normal clone and link post-function always uses the forward link between the issues. The script was patched to allow reverse links as well.

Put the following code in $JIRA_INSTALLATION_DIR/atlassian-jira/WEB-INF/classes/com/onresolve/jira/groovy/canned/workflow/postfunctions/CloneIssue.groovy:

package com.onresolve.jira.groovy.canned.workflow.postfunctions
 
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.config.ConstantsManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.link.IssueLinkManager
import com.atlassian.jira.issue.link.IssueLinkType
import com.atlassian.jira.issue.link.IssueLinkTypeManager
import com.atlassian.jira.util.ErrorCollection
import com.atlassian.jira.util.SimpleErrorCollection
import com.onresolve.jira.groovy.canned.CannedScript
import com.onresolve.jira.groovy.canned.utils.CannedScriptUtils
import com.onresolve.jira.groovy.canned.utils.ConditionUtils
 
class CloneIssue extends CopyIssueWithAttachments implements CannedScript{
 
    public static String FIELD_LINK_DIRECTION = "FIELD_LINK_DIRECTION"
 
    String getName() {
        return "Clones an issue and links."
    }
 
    String getDescription() {
        return """Clones this issue to another issue, optionally in another project, and optionally a different issue type.
        """
    }
 
    List getCategories() {
        ["Function", "Listener"]
    }
 
    List getParameters(Map params) {
        [
            ConditionUtils.getConditionParameter(),
            [
                Name:FIELD_TARGET_PROJECT,
                Label:"Target Project",
                Type: "list",
                Description:"Target project. Leave blank for the same project as the source issue.",
                Values: CannedScriptUtils.getProjectOptions(true),
            ],
            [
                Name:FIELD_TARGET_ISSUE_TYPE,
                Label:"Target Issue Type",
                Type: "list",
                Description:"""Target issue type. Leave blank for the same issue type as the source issue.
                    <br>NOTE: This issue type must be valid for the target project""",
                Values: CannedScriptUtils.getAllIssueTypes(true),
            ],
            getOverridesParam(),
            [
                Name:FIELD_LINK_TYPE,
                Label:'Issue Link Type',
                Type: "list",
                Description:"What link type to use to create a link to the cloned record.",
                Values: CannedScriptUtils.getAllLinkTypes(true),
            ],
            [
                Name:FIELD_LINK_DIRECTION,
                Label:'Link direction',
                Type: "list",
                Description:"What should be the direction of the link",
                Values: [NORMAL:'Normal', REVERSE:'Reverse'],
            ],
        ]
    }
 
    public ErrorCollection doValidate(Map params, boolean forPreview) {
        SimpleErrorCollection errorCollection = new SimpleErrorCollection()
        if (!params[FIELD_LINK_TYPE]) {
            errorCollection.addError(FIELD_LINK_TYPE, "You must provide a link type.")
        }
        // todo: validation for issue type if set
        return errorCollection
    }
 
    Map doScript(Map params) {
        MutableIssue issue = params['issue'] as MutableIssue
 
        Boolean doIt = ConditionUtils.processCondition(params[ConditionUtils.FIELD_CONDITION] as String, issue, false, params)
        if (! doIt) {
            return [:]
        }
 
        params = super.doScript (params)
 
        Issue newIssue = params['newIssue'] as Issue
 
 
        String linkTypeId = params[FIELD_LINK_TYPE] as String
 
        // get the current list of outwards depends on links to get the sequence number
        IssueLinkManager linkMgr = ComponentManager.getInstance().getIssueLinkManager()
 
        if (linkTypeId && linkMgr.isLinkingEnabled()) {
            IssueLinkTypeManager issueLinkTypeManager = (IssueLinkTypeManager) ComponentManager.getComponentInstanceOfType(IssueLinkTypeManager.class)
 
            IssueLinkType linkType = issueLinkTypeManager.getIssueLinkType(linkTypeId as Long)
 
            if (linkType) {
                if ("REVERSE".equals(params[FIELD_LINK_DIRECTION])) {
                    linkMgr.createIssueLink (newIssue.genericValue.id, issue.id, linkType.id, 0, getUser(params))
                } else {
                    linkMgr.createIssueLink (issue.id, newIssue.genericValue.id, linkType.id, 0, getUser(params))
                }
            }
            else {
                log.warn ("No link type $linkTypeId found")
            }
        }
 
        params
    }
 
 
    String getDescription(Map params, boolean forPreview) {
        ConstantsManager constantsManager = ComponentManager.getInstance().getConstantsManager()
 
        StringBuffer sb = new StringBuffer()
        sb << getName() + "<br>Issue will be cloned to project " + (params[FIELD_TARGET_PROJECT] ?: "same as parent")
        sb << "<br>With issue type: " + (params[FIELD_TARGET_ISSUE_TYPE] ? constantsManager.getIssueTypeObject(params[FIELD_TARGET_ISSUE_TYPE] as String)?.name : "same as parent")
        sb.toString()
    }
 
    public Boolean isFinalParamsPage(Map params) {
        true
    }
 
}

The patch has been submitted for inclusion in future versions of the plug-in, see GRV-124.

2 Comments

  1. Hello Joachim,

    Under “Transition parent”, you seem to imply that the code example will assure that the parent is marked as “in progress” when progress is started on a sub-task, which is something we would like.

    However, the actual code given (also attached to the GRV ticket) is for resolving the parent when all subtasks are resolved (a feature already available in Jira itself I believe).

    Thanks,
    Johnny

    • joachim says:

      Have you tried? The code looks fine to me (and it is a while sonce I did this). I think the confision is caused by the WorkflowUtils.resolveIssue() method doing the given action which “resolves” the current workflow task which (depending on the selectin actionId) may or may not be the same as “resolving” the issue.

Leave a Reply

Your email address will not be published. Required fields are marked *

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

*