Hi all,
We are experiencing an issue with transitioning an issue using a for loop in a post function script. Somehow when an issue that we want to transition using the script has satisfied the condition we set up, it successfully transitions it. But if there are multiple issues that is needed to be transitioned, what happens is that only the first issue it sees is transitioned and then the script stops. We have checked and verified that the loop works as without the line containing the "issueService.Transition", it completes the loop but without the transition.
Adding our script here for reference:
import com.atlassian.jira.bc.issue.search.SearchService import com.atlassian.jira.bc.issue.IssueService import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.issue.search.SearchException import com.atlassian.jira.issue.Issue import com.atlassian.jira.issue.link.IssueLink import com.atlassian.jira.issue.link.IssueLinkManager import com.atlassian.jira.issue.CustomFieldManager import com.atlassian.jira.web.bean.PagerFilter import com.atlassian.jira.user.ApplicationUser import com.atlassian.jira.workflow.TransitionOptions
final String jqlSearch = "issue in linkedIssues(${issue})" // the jql query for the linked issue
log.warn("issue: " + issue) def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def searchService = ComponentAccessor.getComponentOfType(SearchService) SearchService.ParseResult parseResult = searchService.parseQuery(user, jqlSearch)
if (parseResult.isValid()) {
//try {
def results = searchService.search(user, parseResult.query, PagerFilter.unlimitedFilter) def parentIssues = results.results // jql query result
log.warn("Number of parent issues: " + parentIssues.size())
log.warn("Checking parent issue(es) and status")
for ( def i = 0; i < parentIssues.size(); i++) {
def parentIssueStatus = parentIssues[i].getStatus().getName()
log.warn("Parent issue and status: " + parentIssues[i].key + " : " + parentIssues[i].getStatus().getName())
log.warn("Checking linked issue(es) and status")
if ( parentIssues[i].issueType.name == "Test-Case" ) {
final String jqlQuery = "issue in linkedIssues(${parentIssues[i].key})" // the jql query for the parent issue
SearchService.ParseResult parseNewResult = searchService.parseQuery(user, jqlQuery)
if (parseNewResult.isValid()) {
def linkedResults = searchService.search(user, parseNewResult.query, PagerFilter.unlimitedFilter)
def linkedIssues = linkedResults.results // jql query result for linked issues on parent issue
for (link in linkedIssues) {
def linkedIssueStatus = link.getStatus().getName()
log.warn("Linked issue and status: " + link.key + " : " + link.getStatus().getName())
}
def statusCheckerDefect = linkedIssues.count { it.getStatus().getName().contains("Closed") } // Number of linked defect issues that has Passed Status
log.warn("Total number of linked issues: " + statusCheckerDefect)
def linkedIssuesCount = linkedIssues.size() - 1 // Number of linked issues
log.warn("Number of linked issues associated with parent ticket: " + linkedIssuesCount)
if ( linkedIssuesCount == statusCheckerDefect) { // this block for transition for blocked to unblocked and failed to for retest
IssueService issueService = ComponentAccessor.getIssueService()
if (parentIssueStatus == "Blocked") {
IssueService.TransitionValidationResult result = issueService.validateTransition(user, parentIssues[i].getId(), 201, issueService.newIssueInputParameters())
if (result.isValid()) {
log.warn("Moving ticket from Blocked to Unblocked")
issueService.transition(user, result)
log.warn("Continuing loop")
} else {
log.warn result.getErrorCollection().getErrors()
}
log.warn("Continuing loop2")
}
if (parentIssueStatus == "Failed") {
IssueService.TransitionValidationResult result = issueService.validateTransition(user, parentIssues[i].getId(), 241, issueService.newIssueInputParameters())
if (result.isValid()) {
log.warn("Moving ticket from Failed to For Retest")
issueService.transition(user, result)
log.warn("Continuing loop")
} else {
log.warn result.getErrorCollection().getErrors()
} log.warn("Continuing loop2")
} log.warn("Continuing loop3")
} log.warn("Continuing loop4")
} log.warn("Continuing loop5")
} log.warn("Continuing loop6")
}
} else {
log.warn("Invalid query")
return null
}
I can't see anything inherently wrong. So maybe something is going on during the transition.
A few things to try:
1) assign the issueService.transition result to some variable and examine the results
2) Don't put the log.warn() statements on the same line as the closing brace or separate them with ;
3) change the for loops for groovy each construct
2) send the issue service into a separate thread so that it can't interact with the current thread (will also cause the transitions to happen asynchronously... useful if you have many).
Here is a rewritten script (untested) that applies all of this along with some groovyfication ;)
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.web.bean.PagerFilter
def issueService = ComponentAccessor.issueService
def searchService = ComponentAccessor.getComponentOfType(SearchService)
def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def statusTransitionMap = [Blocked: 201, Failed: 241]
final String jqlSearch = "issue in linkedIssues(${issue.key})" // the jql query for the linked issue
log.warn("issue: " + issue)
def parseResult = searchService.parseQuery(user, jqlSearch)
if (parseResult.isValid()) {
def results = searchService.search(user, parseResult.query, PagerFilter.unlimitedFilter)
def linkedIssues = results.results // jql query result
log.warn("Number of linked issues: " + linkedIssues .size())
log.warn("Checking linked issue(es) and status")
linkedIssues.
findAll{it.issueType.name == 'Test-Case'}.
each{ testCaseIssue ->
Thread.start{
def jqlQuery = "issue in linkedIssues($testCaseIssue.key)"
def parseNewResult = searchService.parseQuery(user, jqlQuery)
if (parseNewResult.isValid()) {
def linkedResults = searchService.search(user, parseNewResult.query, PagerFilter.unlimitedFilter)
//for jira 8.3
/*linkedResults.results*.document.collect{
ComponentAccessor.issueManager.getIssueObject(it.getValues('issue_id').first() as Long)
}*/
def subLinkedIssues = linkedResults.results
def closedLinkedIssues = subLinkedIssues.findAll{it.status.name.contains('Closed')}
log.warn "Linked Issues and status associated with test case issue ($testCaseIssue.key): \n${subLinkedIssues.collect{ " $it.key : $it.status.name"}.join('\n')}"
if( subLinkedIssues.size() == closedLinkedIssues.size()) {
def transitionId = statusTransitionMap[testCaseIssue .status.name]
def validationResult = issueService.validateTransition(user, testCaseIssue.id, transitionId, issueService.newIssueInputParameters())
if(validationResult.isValid()){
log.warn "Moving ticket from $testCaseIssue.status.name using transition id= $transitionId"
def result = issueService.transition(user, validationResult)
} else {
log.error validationResult.errorCollection.errors
}
} else {
log.warn "Some linked issues are still blocked"
}
}
}
}
} else {
log.warn("Invalid query")
return null
}
Hi Peter, thanks for the insights we will try it out and let you know.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
After applying the changes you recommended, the same issue still happens. Somehow there seems to be some issue with the issueService.transition line. Would you know of any other way to perform transition aside from using issueService.transition?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Maybe you can try it on a single issue in the console and try to narrow down the source of the issue.
For example, I took the script I suggested and pared it down to just the transition piece and added a manual issue definition (and added a statu/transition map item that matches my workflow):
import com.atlassian.jira.component.ComponentAccessor
def issueService = ComponentAccessor.issueService
def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def statusTransitionMap = [Blocked: 201, Failed: 241, Closed: 61]
def issue = ComponentAccessor.issueManager.getIssueObject('JSP-1922')
log.warn("issue: " + issue)
def transitionId = statusTransitionMap[issue.status.name]
def validationResult = issueService.validateTransition(user, issue.id, transitionId, issueService.newIssueInputParameters())
if(validationResult.isValid()){
log.warn "Moving ticket from $issue.status.name using transition id= $transitionId"
def result = issueService.transition(user, validationResult)
} else {
log.error validationResult.errorCollection.errors
}
And I executed this in the console. My issue was initially Closed, and it was transitioned back to New using the "Reopen task" transition (id=61).
And it generated the following expected logs:
2020-05-28 08:37:35,738 WARN [runner.ScriptBindingsManager]: issue: JSP-1922
2020-05-28 08:37:35,886 WARN [runner.ScriptBindingsManager]: Moving ticket from Closed using transition id= 61
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
After trying it out, we have the following weird findings:
1. Somehow, trying it out in script console when all the issues are in the proper status (for example, the defect that should trigger the script is in Closed status while the test-case(s) that are supposed to be affected by this script is in Blocked status) causes it to work on more than one test-case but only one transition shows up in the logs.
2. Converting the script as a listener also has this weird thing. We used "All Events" trigger for the listener with the same issue status condition as #1 (We made some flags for it to make sure it works only for the intended purpose). The problem with this is that when the listener is triggered from a transition event (we use a generic event here), it only works for the first transition. But when triggered by adding a comment on the defect with "Closed" status that should trigger the script, somehow it is able to execute transition on all the test-case that are in the "Blocked" status.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
I got terribly confused while reading your code and trying to understand the relationship between issues and which issues you needed to transition and why.
I suspect there is something weird happening in there.
Can you explain those relationships and your requirements?
Maybe I can help suggest a different way to identify the issues to act on rather than the double-nesting of JQL search that's currently happening.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Apologies for the confusion.
The requirement we have is to automatically transition all issue(s) in our test board from Failed status and/or Blocked status to For Retest status and/or Unblocked status when all their linked issues is in Closed status.
Since we were troubled on how to start, we decided to instead use the defect board issues as it would trigger the automatic transition when it is transitioned to Closed status. This is the reason for using the 2 layered JQL search.
We hope this clears some things.
We are grateful for your assistance.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
So something like this in JQL?
status in ( Failed, Blocked) and issueFunction not in linkedIssuesOf("status != Closed")
This should list all the Failed/Blocked issues that do not have any open linked issues.
Then you should only have a single layer to deal with.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Thanks for the insight, I think this approach is cleaner than what we have.
I think I am seeing the issue now. Somehow when the script executes if the linked issue associated with an issue from the test board is transitioned to Closed status, JQL search shows the status of said issue to be its pre-transition status instead of Closed status.
Example:
ABC-1 (from defect board) is a defect linked to both ABC-2 and ABC-3 which are in the test board. When ABC-1 is transitioned to "Closed" status from "In Progress" status, the script triggers to check if the issue(s) linked to the test board issues (ABC-2 and ABC-3 in this case). Somehow we could see that for some reason, the JQL search returns the status of ABC-1 as "In Progress" instead of "Closed".
Have you encountered this by any chance?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
The JQL search are based on the index.
So if you've updated the status of one issue, but the corresponding index was not refreshed, then the JQL search will be based on the old status.
But if you get an issue object and actually validate the status, you should be able to find the correct/current status.
The documentation for transition method does indicate that re-indexing should be taking place:
This method will store the provided issue to the JIRA datastore and will transition it through workflow. Use this method to perform the default behavior for updating and transitioning an issue in JIRA. The issue will be saved and re-indexed, the comment, if provided, will be added and the transition will be made.
If you've implemented by threading suggestion, that might explain it, the thread/indexing may not be complete when you re-evaluate the jql. Which is why having a single JQL would be better: get all the issues you want to change in one search, then act on each of them. The action on one issue should not impact the others.
If you didn't implement the threading, then I really don't know why your JQL data is inconsistent.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
I see, thanks for the assistance. We will be reaching out to atlassian support in case they know a way to resolve this.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.