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.
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
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.