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