Identifying Orphan Subtasks in Jira

It is entirely possible to set up Jira so that a subtask may remain open, while the parent task is closed. This effectively creates orphan subtasks, not connected to any open issue or ticket.

Identifying these is a matter of first identifying all subtasks, and then checking the status of both the subtask and its parent.

We first identify all subtasks for a given project by invoking a service context, and running some JQL against the Jira instance:

import com.atlassian.jira.bc.filter.SearchRequestService
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.jira.bc.JiraServiceContext
import com.atlassian.jira.bc.JiraServiceContextImpl
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.issue.label.LabelManager

def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchProvider = ComponentAccessor.getComponent(SearchProvider)
def issueManager = ComponentAccessor.getIssueManager()
def searchService = ComponentLocator.getComponent(SearchService)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def searchManager = ComponentLocator.getComponent(SearchRequestService)
def contextManager = ComponentLocator.getComponent(JiraServiceContext)
def searchRequest = ComponentLocator.getComponent(SearchRequest)
def labelManager = ComponentLocator.getComponent(LabelManager)



JiraServiceContextImpl serviceCtx = new JiraServiceContextImpl(user);
//Declare a search context using the logged-in user

def queryParser = ComponentAccessor.getComponent(JqlQueryParser)
//Declare a parser to handle the JQL query

def query = queryParser.parseQuery('project = "<project name>" ')
//Define the JQL query.  In this instance we're returning all issues under a given project

def search = searchService.search(user, query, PagerFilter.getUnlimitedFilter())
//Define a search, using all the pieces defined so far

 

Naturally you would fill in “project name” with the name of the target project.   This service context allows us to define a search service. The search service takes a user and a query as input.  In the context above, we’ve defined “user” as whomever is logged into the system and is running the script.

So we’re able to run a search. Now what?

By invoking the results of the search, we’re able to iterate through the list:

search.results.each {
//Iterate over the results

  retrievedIssue ->
//Do something with the results

}

 

The next step is to identify any issue that is a subtask.  There are a number of ways that this could be accomplished.  One of the ways is to simply check if the issue has a parent task. If it has a parent, it must therefor be a subtask! We actually start by first handling anything that is not a subtask, as the script will otherwise throw a null error.  In effect, we’ve told the script to do something with the issue so long as it has a parent task.

if (retrievedIssue.getParentObject() == null) {
  //We determine if an issue is a subtask by testing for a parent object
      
  } else {

 

Next we need to identify any issue with a parent object with a status of “closed”, but which itself is not “closed”:

    if (retrievedIssue.getParentObject().getStatus().name == "Closed") {
	//If the parent object's status is closed
      if (retrievedIssue.getStatus().name != "Closed") {
		//And if the subtask/child issue's status is NOT closed

 

The result would be logged or added to a string buffer.  Finally, we put it all together:

import com.atlassian.jira.bc.filter.SearchRequestService
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.jira.bc.JiraServiceContext
import com.atlassian.jira.bc.JiraServiceContextImpl
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.issue.label.LabelManager

def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchProvider = ComponentAccessor.getComponent(SearchProvider)
def issueManager = ComponentAccessor.getIssueManager()
def searchService = ComponentLocator.getComponent(SearchService)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def searchManager = ComponentLocator.getComponent(SearchRequestService)
def contextManager = ComponentLocator.getComponent(JiraServiceContext)
def searchRequest = ComponentLocator.getComponent(SearchRequest)
def labelManager = ComponentLocator.getComponent(LabelManager)



JiraServiceContextImpl serviceCtx = new JiraServiceContextImpl(user);
//Declare a search context using the logged-in user

def queryParser = ComponentAccessor.getComponent(JqlQueryParser)
//Declare a parser to handle the JQL query

def query = queryParser.parseQuery('project = "WFCST" ')
//Define the JQL query.  In this instance we're returning all issues under a given project

def search = searchService.search(user, query, PagerFilter.getUnlimitedFilter())
//Define a search, using all the pieces defined so far

search.results.each {
//Iterate over the results
  retrievedIssue ->

  if (retrievedIssue.getParentObject() == null) {
  //We determine if an issue is a subtask by testing for a parent object
      
  } else {
    if (retrievedIssue.getParentObject().getStatus().name == "Closed") {
	//If the parent object's status is closed
      if (retrievedIssue.getStatus().name != "Closed") {
		//And if the subtask/child issue's status is NOT closed
          
        log.warn("This subtask is open, but has a closed parent: " + retrievedIssue.getKey()) 
        //If the parent is closed but the child is not closed, we must have an orphan, and that should be logged
      }
    }
  }
}

 

This script limits the search query to a single project. It’s quite trivial to extend this script to parse ALL projects in a Jira instance:

import com.atlassian.jira.bc.filter.SearchRequestService
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.jira.bc.JiraServiceContext
import com.atlassian.jira.bc.JiraServiceContextImpl
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.search.SearchProvider
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.issue.label.LabelManager

def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchProvider = ComponentAccessor.getComponent(SearchProvider)
def issueManager = ComponentAccessor.getIssueManager()
def searchService = ComponentLocator.getComponent(SearchService)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def searchManager = ComponentLocator.getComponent(SearchRequestService)
def contextManager = ComponentLocator.getComponent(JiraServiceContext)
def searchRequest = ComponentLocator.getComponent(SearchRequest)
def prList = ComponentAccessor.getProjectManager().getProjectObjects().key

def sb = []
//Define a string buffer to hold the results


JiraServiceContextImpl serviceCtx = new JiraServiceContextImpl(user);
//Declare a search context using the logged-in user

def queryParser = ComponentAccessor.getComponent(JqlQueryParser)
//Declare a parser to handle the JQL query

prList.each {
  projectName ->

    def query = queryParser.parseQuery('project = "' + projectName + '" ')
  //Define the JQL query.  In this instance we're feeding the name of each project into the JQL on each iteration of the loop

  def search = searchService.search(user, query, PagerFilter.getUnlimitedFilter())
  //Define a search, using all the pieces defined so far

  search.results.each {
    //Iterate over the results
    retrievedIssue ->

      if (retrievedIssue.getParentObject() == null) {
        //We determine if an issue is a subtask by testing for a parent object

      } else {
        if (retrievedIssue.getParentObject().getStatus().name == "Closed") {
          //If the parent object's status is closed
          if (retrievedIssue.getStatus().name != "Closed") {
            //And if the subtask/child issue's status is NOT closed

            sb.add("This subtask is open, but has a closed parent: " + retrievedIssue.getKey() + "</br>")
            //If the parent is closed but the child is not closed, we must have an orphan, and that should be logged
          }
        }
      }
  }
}
return sb

 

Notice that we’ve defined prList as a list of every project key in Jira. We then loop through the list, and feed each key into the JQL. 

Leave a Reply

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