Find latest created issue in project created between 4AM-4PM UTC

Ian Balas June 9, 2021

Our instance uses a modified version of Adaptavist's Round Robin Script. In the code block that pulls the last issue with an assignee, I changed it to pull 'Defect' issues that have an assignee and also a value in a custom field called 'Original Assignee':

 def lastIssueIdWithAssignee = issueManager.getIssueIdsForProject(issue.projectObject.id)
.sort()
.reverse()
.find {
def foundIssue = issueManager.getIssueObject(it)
return foundIssue.assignee && foundIssue.issueType.name == "Defect" && foundIssue.getCustomFieldValue(originalAssigneeCustomField)
}

I want to add to the return statement and make it so that it pulls the last issue that fulfills the above criteria, but have it also look at the created date/time. In other words, I want it to pull the last created issue that:

  • is of 'Defect' Issue type
  • has an assignee
  • has a value in the 'Original Assignee' custom field

and in addition...

  • was created in between the times of 4:00AM - 4:00 PM UTC

 

Is this possible? If so, how would I translate it into code?

1 answer

1 accepted

1 vote
Answer accepted
Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
June 9, 2021

I haven't used or tested the round robin script. But I have some concern about performance on projects with lots of issues. The more issue to have the more issues you have to sort and reverse. (Since this deals with just a list of long integer, it may not be a valid concern in most normal use case)

But given that concern along with your requirements, I was lead to think about leveraging JQL.  

Here is a console snippet that should return the last issue assigned according to your criteria:

import java.time.ZoneOffset
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.component.ComponentAccessor

def searchService = ComponentAccessor.getComponent(SearchService)
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issue = ComponentAccessor.issueManager.getIssueObject('ASP-411') //CHANGE THIS TO A SAMPLE ISSUE TO TEST
def jql = /project = $issue.projectObject.id and "Original Assignee" is not empty and assignee is not empty and issueType=Defect and Created > -30d order by Created desc/


def parseResult = searchService.parseQuery(currentUser, jql)
def lastAssignee
if(parseResult.isValid()){
def results = searchService.search(currentUser, parseResult.query,PagerFilter.unlimitedFilter)
log.info results.total
lastAssignee = results.results.find{
def hour = it.created.toInstant().atZone(ZoneOffset.UTC).hour
hour >=4 && hour < 16
}?.assignee
}
return lastAssginee

Plug that into the rest of the script (adjusting for the fact that you will have a user object instead of issueId).

Ian Balas June 15, 2021

Hi @Peter-Dave Sheehan ,

Thanks for the reply! My apologies for the delay in response.

I've plugged in the snippet as follows:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.security.roles.ProjectRoleManager

import java.time.ZoneOffset

import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService

def searchService = ComponentAccessor.getComponent(SearchService)
def issueManager = ComponentAccessor.issueManager

if (!!issue.assignee){
return
} else {
def utcHour = new Date().toInstant().atOffset( ZoneOffset.ofTotalSeconds(0)).hour
if (utcHour >= 4 && utcHour < 16) {
// The role you want assignees to set from
final roleName = 'Operations'
final originalAssigneeCustomFieldName = "customfield_20321"

// If it is true, the assigned issues will be reassigned
final reassignedIssues = true

def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager)
def customFieldManager = ComponentAccessor.getCustomFieldManager()

def originalAssigneeCustomField = customFieldManager.getCustomFieldObject(originalAssigneeCustomFieldName)

// Get all of the users associated with the specified project role
def projectRole = projectRoleManager.getProjectRole(roleName)

// Sort the users of the project role using the user key
def users = projectRoleManager.getProjectRoleActors(projectRole, issue.projectObject)
.applicationUsers
.toSorted { it.key }

// There are no users in the specific project role
if (!users) {
log.info ("No users for project role $roleName")
return
}

if (!reassignedIssues && issue.assignee) {
log.info ('The issue is already assigned')
return
}

// Find the latest created issue id that has an assignee
def lastIssueIdWithAssignee = issueManager.getIssueIdsForProject(issue.projectObject.id)
.sort()
.reverse()
.find {
def foundIssue = issueManager.getIssueObject(it)
def jql = /project = $issue.projectObject.id and "Original Assignee" is not empty and assignee is not empty and issueType=Defect and Created > -30d order by Created desc/
def parseResult = searchService.parseQuery(foundIssue.assignee, jql)
def lastAssigned
if (parseResult.isValid()) {
def results = searchService.search(foundIssue.assignee, parseResult.query, PagerFilter.unlimitedFilter)
log.info results.total
lastAssigned = results.results.find {
def hour = it.created.toInstant().atZone(ZoneOffset.UTC).hour
hour >= 4 && hour < 16

}?.assignee
}
return lastAssigned //foundIssue.assignee && foundIssue.issueType.name == "Defect" && foundIssue.getCustomFieldValue(originalAssigneeCustomField)
}

// If no issue fulfilling above criteria is found, new cert issue is assigned to the first user in rr queue
if (!lastIssueIdWithAssignee) {
issue.setAssignee(users.first())
def changeHolder = new DefaultIssueChangeHolder()
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, users.first()), changeHolder)
return
}

// If issue is found, then assignee is based on the Original Assignee and Assignee fields:
// * assuming Original Assignee field is not empty, user in said field is taken, and issue assigns to
//the next user in queue
def lastIssue = issueManager.getIssueObject(lastIssueIdWithAssignee)

def lastAssignee = lastIssue.getCustomFieldValue(originalAssigneeCustomField) ?: lastIssue.assignee
def lastAssigneeIndex = users.indexOf(lastAssignee)
def nextAssignee = users[(lastAssigneeIndex + 1) % users.size()]

issue.setAssignee(nextAssignee)
def changeHolder = new DefaultIssueChangeHolder()
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, nextAssignee), changeHolder)

def prevIssueId = issueManager.getIssueIdsForProject(issue.projectObject.id)
.sort()
.reverse()
.find {
def foundIssue = issueManager.getIssueObject(it)
return foundIssue.assignee && foundIssue.issueType.name == "Defect"
}
def prevIssue = issueManager.getIssueObject(prevIssueId)

def prevAssignee = prevIssue.assignee
def prevAssigneeIndex = users.indexOf(prevAssignee)

if (prevAssignee == nextAssignee){
def nextAssigneeAfter = users[(lastAssigneeIndex + 2) % users.size()]

issue.setAssignee(nextAssigneeAfter)
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, nextAssigneeAfter), changeHolder)
}
}
}

And I hear you on the concern about the issues to cycle and reverse through as more issues get created. I assume the JQL will help keep the runtime to a minimum if this just pulls the issues created in the last 30 days, so thank you for that!

I will be testing this tomorrow between the hours of 4AM-4PM UTC and come back with results!

Appreciate the help again as usual. 

Ian Balas June 16, 2021

Hi @Peter-Dave Sheehan 

After testing today around 4AM-4PM UTC timeframe, I don't think this worked unfortunately :(

So here's how I directly plugged your snippet into the lastIssueWithAssignee block:

// Find the latest created issue id that has an assignee
def lastIssueIdWithAssignee = issueManager.getIssueIdsForProject(issue.projectObject.id)
.sort()
.reverse()
.find {
def foundIssue = issueManager.getIssueObject(it)
def jql = /project = $issue.projectObject.id and "Original Assignee" is not empty and assignee is not empty and issueType=Defect and Created > -30d order by Created desc/
def parseResult = searchService.parseQuery(foundIssue.assignee, jql)
def lastAssigned
if (parseResult.isValid()) {
def results = searchService.search(foundIssue.assignee, parseResult.query, PagerFilter.unlimitedFilter)
log.info results.total
lastAssigned = results.results.find {
def hour = it.created.toInstant().atZone(ZoneOffset.UTC).hour
hour >= 4 && hour < 16
}?.assignee
}
return lastAssigned

This is the list of users in the 'Operations' role that are to be assigned in round robin between 4AM-4PM UTC (in order):

1) Ary D

2) Chen S

3) David Y

4) Dominik M

5) Fernando B

6) Gamal D

7) Rasanga S

 

The last issue that was assigned to someone in that timeframe from the previous day was 'David Y'.

Now today, entering the 4AM-4PM UTC timeframe again, I created another issue and expected that it would auto-assign to 'Dominik M', picking right back up where it left off from the other day. It instead defaulted to the first user in the list of 'Operations' users ('Ary D').

-----

Also I tried running the snippet through console, but doesn't seem to work, stating that lastAssignee has to be declared:

Screen Shot 2021-06-16 at 11.01.52 AM.png

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
June 16, 2021

Looks like there was a typo in my snippet.

The last line reads lastAssiginee  and should be lastAssignee (it should match what's on line 13 and 17)

Try to run that in the console again and see if you get the correct user.

Then in your main code, I'm not sure it's wise to have my suggested snipped into the block that will contains all issues (getIssueIdsForProject() will include every issues in your project, the whole point of using JQL is to avoid this.

So instead of 

// Find the latest created issue id that has an assignee
def lastIssueIdWithAssignee = issueManager.getIssueIdsForProject(issue.projectObject.id)
.sort()
.reverse()
.find {
def foundIssue = issueManager.getIssueObject(it)
def jql = /project = $issue.projectObject.id and "Original Assignee" is not empty and assignee is not empty and issueType=Defect and Created > -30d order by Created desc/
def parseResult = searchService.parseQuery(foundIssue.assignee, jql)
def lastAssigned
if (parseResult.isValid()) {
def results = searchService.search(foundIssue.assignee, parseResult.query, PagerFilter.unlimitedFilter)
log.info results.total
lastAssigned = results.results.find {
def hour = it.created.toInstant().atZone(ZoneOffset.UTC).hour
hour >= 4 && hour < 16
}?.assignee
}
return lastAssigned //foundIssue.assignee && foundIssue.issueType.name == "Defect" && foundIssue.getCustomFieldValue(originalAssigneeCustomField)
}

Here is how I would incorporate that JQL snippet into your whole script

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.security.roles.ProjectRoleManager

import java.time.ZoneOffset

import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService

def searchService = ComponentAccessor.getComponent(SearchService)
def issueManager = ComponentAccessor.issueManager
def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager)
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser


// The role you want assignees to set from
final roleName = 'Operations'

final originalAssigneeCustomFieldName = "customfield_20321"
def originalAssigneeCustomField = customFieldManager.getCustomFieldObject(originalAssigneeCustomFieldName)

// If it is true, the assigned issues will be reassigned
final reassignIssues = false

if (issue.assignee && !reassignIssues ){
//issue is already assigned, and we don't want to re-assign
return
}

def createdUtcHour = issue.created.toInstant().atOffset( ZoneOffset.ofTotalSeconds(0)).hour
if (createdUtcHour <= 4 || createdUtcHour >= 16) {
//issue was not created in the in target time range, nothing to do
return
}

// Get all of the users associated with the specified project role
def projectRole = projectRoleManager.getProjectRole(roleName)

// Sort the users of the project role using the user key
def users = projectRoleManager.getProjectRoleActors(projectRole, issue.projectObject)
.applicationUsers
.toSorted { it.key }

// There are no users in the specific project role
if (!users) {
log.info ("No users for project role $roleName")
return
}

//get recent issues in reverse order of creation
def jql = /project = $issue.projectObject.id and "Original Assignee" is not empty and assignee is not empty and issueType=Defect and Created > -30d order by Created desc/

def parseResult = searchService.parseQuery(currentUser, jql)
def lastAssignee

assert parseResult.isValid() : "The following JQL was not valid: $jql"

def results = searchService.search(currentUser, parseResult.query,PagerFilter.unlimitedFilter)
log.info "Found $results.total assigned defects in the last 30 days for project $issue.projectObject.key"

//find the first isstance of an issue created between 4-16 UTC (this will be the last one created since were order by Created desc)
def lastIssueInTimeRange = results.results.find{
def hour = it.created.toInstant().atZone(ZoneOffset.UTC).hour
hour >=4 && hour < 16
}


def nextAssignee

if(!lastIssueInTimeRange){
log.info "No isssue was found created between 4-16UTC in the last 30 days. Using the first role member"
nextAssignee = users.first()
} else {
//examine the issues's Original Assignee and assignee
lastAssignee = lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField) ?: lastIssueInTimeRange.assignee
//get the next user in line
def lastAssigneeIndex = users.indexOf(lastAssignee)
nextAssignee = users[(lastAssigneeIndex + 1) % users.size()]
}


issue.setCustomFieldValue(originalAssigneeCustomField, nextAssignee)
issue.setAssignee(nextAssignee)


 

This will only run in a postfunction.

 

If you want to run the same thing in the console (for testing).

Add after your issue manager:

issueManager.getIssueObject('somekey')

And at the end:

def issueService =ComponentAccessor.issueService
def input = issueService.newIssueInputParameters()
input.setAssigneeId(nextAssignee.key)
input.addCustomFieldValue(originalAssigneeCustomField.id as Long, nextAssignee.key)
def validationResult = issueService.validateUpdate(currentUser,issue.id, input)
assert validationResult.isValid()
issueService.update(currentUser, validationResult)
Like Ian Balas likes this
Ian Balas June 18, 2021

@Peter-Dave Sheehan 

I can confirm that your jql snippet solution works perfectly now. Thank you so much!

Also, I've come to realize you're very knowledgeable in coming up with custom-coded solutions like this. To say I'm impressed is an understatement. So I just wanted to inquire if you had references to any learning resources that involve the use of Groovy, whether interpolated with Scriptrunner/any other JIRA plugins, or just the basics. It'd be really helpful as a learning experience!

Again, thanks for all your help on this!

 

-Ian

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
June 18, 2021

Thanks!

Sorry... I don't have any good references other than the obvious like:

I have a curious mind and I like exploring and experimenting. Everything I know I learned through examining the Adaptivist documentation and Script Library, examining other snippets posted here in the community, and general google search. 

Like Ian Balas likes this
Ian Balas July 6, 2021

Hi @Peter-Dave Sheehan ,

Hope you've been well and Happy 4th! I hope you don't mind, but there's something else about this script that I wanted to specifically bring to your attention.

So I'm hoping to load-balance the auto-assignment in this script, and to do so, I'm thinking I might have to update the script again and change the way that the user scope is pulled. As of now, assignees are set from a sorted list of users in the 'Operations' project role. But to implement the load-balancing approach, I want to make it so that certain users are set as the assignee for two tickets in a row before moving to the next user.

With this, I think it'd make more sense to reference from an array of user names set in the script rather than scoping through all users with the 'Operations' role..so to try and implement that myself, I made the following changes:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.security.roles.ProjectRoleManager

import java.time.ZoneOffset

import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService

def searchService = ComponentAccessor.getComponent(SearchService)
def issueManager = ComponentAccessor.issueManager
def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager)
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUserdef searchService = ComponentAccessor.getComponent(SearchService)

final originalAssigneeCustomFieldName = "customfield_20321"
def originalAssigneeCustomField = customFieldManager.getCustomFieldObject(originalAssigneeCustomFieldName)

// If it is true, the assigned issues will be reassigned
final reassignIssues = false

if (issue.assignee && !reassignIssues ){
//issue is already assigned, and we don't want to re-assign
return
}

def createdUtcHour = issue.created.toInstant().atOffset( ZoneOffset.ofTotalSeconds(0)).hour
if (createdUtcHour <= 4 && createdUtcHour >= 16) {
//issue was not created in the in target time range, nothing to do
return
}

//UPDATED
// List of users to iterate through in rr
def users = ["user1","user2","user2","user3","user4","user5", "user6","user7","user8","user9","user10"]

//Find latest issues in reverse order of creation that has an assignee and a value in 'Original Assignee'
def jql = /project = $issue.projectObject.id and "Original Assignee" is not empty and assignee is not empty and issueType=Defect and Created > -30d order by Created desc/


def parseResult = searchService.parseQuery(currentUser, jql)
def lastAssignee

assert parseResult.isValid() : "The following JQL was not valid: $jql"

def results = searchService.search(currentUser, parseResult.query,PagerFilter.unlimitedFilter)
log.info "Found $results.total assigned Defect issues in the last 30 days for project $issue.projectObject.key"

//find the first instance of an issue created between 16-4, or 4PM-4AM UTC (this will be the last one created since were order by Created desc)
def lastIssueInTimeRange = results.results.find{
def hour = it.created.toInstant().atZone(ZoneOffset.UTC).hour
hour >= 4 && hour < 16
}

def nextAssignee

if(!lastIssueInTimeRange){
log.info "No issue was found created between 16-4UTC in the last 30 days. Using the first role member"
nextAssignee = users.first()
} else {
//examine the issues's Original Assignee and assignee
lastAssignee = lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField) ?: lastIssueInTimeRange.assignee
//get the next user in line
def lastAssigneeIndex = users.indexOf(lastAssignee)
nextAssignee = users[(lastAssigneeIndex + 1) % users.size()]
}
def user = ComponentAccessor.userManager.getUserByName(nextAssignee)

issue.setCustomFieldValue(originalAssigneeCustomField, nextAssignee)
issue.setAssignee(user)

def changeHolder = new DefaultIssueChangeHolder()
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, nextAssignee), changeHolder)

 

The main difference here is that I removed the code block that pulled users in the project role:

// Sort the users of the project role using the user key
def users = projectRoleManager.getProjectRoleActors(projectRole, issue.projectObject)
.applicationUsers
.toSorted { it.key }

and added this instead:

// List of users to iterate through in rr
def users = ["user1","user2","user2","user3","user4","user5", "user6","user7","user8","user9","user10"]

while also declaring a user variable to store the string value of the user to be assigned, and had the assignee set as that user:

def user = ComponentAccessor.userManager.getUserByName(nextAssignee)

issue.setCustomFieldValue(originalAssigneeCustomField, nextAssignee)
issue.setAssignee(user)

def changeHolder = new DefaultIssueChangeHolder()
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, nextAssignee), changeHolder)

The main goal of this is to keep in line with the round robin assignemnt order, whhile user2 is set as the assignee and 'original assignee' for two tickets in a row before moving on to user3.

Unfortunately, my changes made the script execution fail. The assignee instead defaulted to the first user in the list ('user1') and didn't fill a value for the 'Original Assignee' field.

2021-07-06 18:40:24,927 ERROR [workflow.AbstractScriptWorkflowFunction]: Workflow script has failed on issue XDEFECTS-958 for user 'ibalas'. View here: https://stage.jira.vzbuilders.com/secure/admin/workflows/ViewWorkflowTransition.jspa?workflowMode=live&workflowName=ONE+Video+-+Video+Customer+Defects+7.0&descriptorTab=postfunctions&workflowTransition=1&highlight=2
java.lang.ClassCastException: java.lang.String cannot be cast to com.atlassian.jira.user.ApplicationUser
at com.atlassian.jira.issue.customfields.impl.UserCFType.getDbValueFromObject(UserCFType.java:84)
at com.atlassian.jira.issue.customfields.impl.AbstractSingleFieldType.createValue(AbstractSingleFieldType.java:144)
at com.atlassian.jira.issue.fields.ImmutableCustomField.createValue(ImmutableCustomField.java:693)
at com.atlassian.jira.issue.fields.ImmutableCustomField.updateValue(ImmutableCustomField.java:410)
at com.atlassian.jira.issue.fields.ImmutableCustomField.updateValue(ImmutableCustomField.java:396)
at com.atlassian.jira.issue.fields.OrderableField$updateValue$2.call(Unknown Source)
at Script86.run(Script86.groovy:85)

Is there anything that you notice that I could fix? Alternatively, is there an easier way that I can achieve the load balancing approach other than what I've done so far?

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 6, 2021

The reason you are getting the error is that you are mixing ApplicationUser entities with Strings of usernames.

So try to give you variable some names that indicate what they carry.

Like:

def userNames = ["user1","user2","user2","user3","user4","user5", "user6","user7","user8","user9","user10"]
def nextAssignee
if(!lastIssueInTimeRange){
log.info "No issue was found created between 16-4UTC in the last 30 days. Using the first role member"
nextAssignee = users.first() //<-- this is an ApplicationUser object already
} else {
//examine the issues's Original Assignee and assignee
lastAssignee = lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField) ?: lastIssueInTimeRange.assignee
//get the next username from userNames array
def lastAssigneeIndex = userNames.indexOf(lastAssignee)
def nextAssigneeName = userNames[(lastAssigneeIndex + 1) % userNames.size()] 
//get an applicationUser object for the username
nextAssignee = ComponentAccessor.userManager.getUserByName(nextAssigneeName)
}

//def user = ComponentAccessor.userManager.getUserName(nextAssginee) // <-- this is no longer needed

But regarding the reason for your change...

So you don't want each user to get assigned 2 tickets before moving on to the next user in the list, only user2 is capable of handling 2 tickets in a row while all others can only handle 1.

Do I have it right?

This might be an issue when the last assignee is user2... the indexOf will always return the first copy of user2 and will always make the next user user2 again. It will never move on to user3.

You might need to think of a completely different round-robin algorithm. You can't use the last assignee to know who the next one will be. You need to look at the last 2 assignees.

Ian Balas July 7, 2021

@Peter-Dave Sheehan 

Good catch. In that case, I don't see it necessary rewriting the script to assign certain users two tickets in a row. I decided to just include another member part of user2's team to indicate that their team is to take on an additional ticket, like so:

def userNames = ["user1","user2","user2","user3","user4","user5", "user6","user7","user8","user9","user10", "user11"]

Now here's what I'm working with:

def nextAssignee

if(!lastIssueInTimeRange){
log.info "No issue was found created between 16-4UTC in the last 30 days. Using the first role member"
nextAssignee = userNames.first() //<-- this is an ApplicationUser object already
} else {
lastAssignee = lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField) ?: lastIssueInTimeRange.assignee
def lastAssigneeIndex = userNames.indexOf(lastAssignee)
def nextAssigneeName = userNames[(lastAssigneeIndex + 1) % userNames.size()]
nextAssignee = ComponentAccessor.userManager.getUserByName(nextAssigneeName)
}

issue.setCustomFieldValue(originalAssigneeCustomField, nextAssignee)
issue.setAssignee(nextAssignee)

def changeHolder = new DefaultIssueChangeHolder()
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, nextAssignee), changeHolder)

Unfortunately, I'm still having trouble setting the next assignee from the user array. Both assignee and 'Original Assignee' still default to user1. Oddly enough, no errors were logged, but on the issue.setAssignee(nextAssignee) line, there's a static type checking error that indicates that I can't set assignee as a java.lang.object. So I think it doesn't see nextAssignee as an applicationUser object.

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 7, 2021

We can address the static type checking error by importing the class ApplicationUser

import com.atlassian.jira.user.ApplicationUser

Then declaring the nextAssginee using that class instead of def

ApplicationUser nextAssignee

This highlighted something that I missed... when no issues are returned in the jql, we must still convert the first user into an ApplicationUser object. I don't know why I thought this was already an ApplicationUser... probably leftover from when the list of users was coming from the role.

ApplicationUser nextAssignee

if(!lastIssueInTimeRange){
log.info "No issue was found created between 16-4UTC in the last 30 days. Using the first role member"
nextAssignee = ComponentAccessor.userManager.getUserByName(userNames.first())
} else {
log.info "The last assigned issue was $lastIssueInTimeRange"
log.info "$lastIssueInTimeRange has originalAssignee=${lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField)}"
log.info "$lastIssueInTimeRange has assignee=${lastIssueInTimeRange.assignee}"
lastAssignee = lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField) ?: lastIssueInTimeRange.assignee
log.info "lastAssignee set to $lastAssignee"
def lastAssigneeIndex = userNames.indexOf(lastAssignee)
def nextAssigneeName = userNames[(lastAssigneeIndex + 1) % userNames.size()]
nextAssignee = ComponentAccessor.userManager.getUserByName(nextAssigneeName)
log.info "nextAssignee will be $nextAssignee"
}

issue.setCustomFieldValue(originalAssigneeCustomField, nextAssignee)
issue.setAssignee(nextAssignee)

def changeHolder = new DefaultIssueChangeHolder()
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, nextAssignee), changeHolder)

I added some logs to see what's happening.

I'm also noticing you don't have any code in there to persist the issue (after setting the assignee). The originalAssignee will be persisted with the updateValue, but not the issue.setAssginee.

I'd recommend using the following instead of your last 4 lines:

def issueService = ComponentAccessor.issueService
def input = issueService.newIssueInputParameters()
input.setAssigneeId(nextAssignee.key)
input.addCustomFieldValue(originalAssigneeCustomField.idAsLong, nextAssignee.key)
def validationResult = issueService.validateUpdate(currentUser, issue.id, input)
assert validationResult.isValid() : validationResult.errorCollection
issueService.update(currentUser, validationResult)
Ian Balas July 13, 2021

Hi @Peter-Dave Sheehan ,

Sorry for the delay on my end, had a few high-priority items to handle. And once again, thank you for the continued support! Really appreciate it.

So I plugged in the above to handle nextAssignee as an applicationUser object, and also replaced the last four lines with the additional snippet to persist the issue as you suggested, and here's what I got now:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.security.roles.ProjectRoleManager
import com.atlassian.jira.user.ApplicationUser

import java.time.ZoneOffset

import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.search.SearchService

def searchService = ComponentAccessor.getComponent(SearchService)
def issueManager = ComponentAccessor.issueManager
def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager)
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUserdef searchService = ComponentAccessor.getComponent(SearchService)

final originalAssigneeCustomFieldName = "customfield_20321"
def originalAssigneeCustomField = customFieldManager.getCustomFieldObject(originalAssigneeCustomFieldName)

// If it is true, the assigned issues will be reassigned
final reassignIssues = false

if (issue.assignee && !reassignIssues ){
//issue is already assigned, and we don't want to re-assign
return
}

def createdUtcHour = issue.created.toInstant().atOffset( ZoneOffset.ofTotalSeconds(0)).hour
if (createdUtcHour <= 4 && createdUtcHour >= 16) {
//issue was not created in the in target time range, nothing to do
return
}

def userNames = ["userkey1","userkey2","userkey3","userkey4","userkey5", "userkey6","userkey7","userkey8","userkey9","userkey10", "userkey11"]

//Find latest issues in reverse order of creation that has an assignee and a value in 'Original Assignee'
def jql = /project = $issue.projectObject.id and "Original Assignee" is not empty and assignee is not empty and issueType=Defect and Created > -30d order by Created desc/

def parseResult = searchService.parseQuery(currentUser, jql)
def lastAssignee

assert parseResult.isValid() : "The following JQL was not valid: $jql"

def results = searchService.search(currentUser, parseResult.query,PagerFilter.unlimitedFilter)
log.info "Found $results.total assigned Defect issues in the last 30 days for project $issue.projectObject.key"

//find the first instance of an issue created between 16-4, or 4PM-4AM UTC (this will be the last one created since were order by Created desc)
def lastIssueInTimeRange = results.results.find{
def hour = it.created.toInstant().atZone(ZoneOffset.UTC).hour
hour >= 4 && hour < 16
}

ApplicationUser nextAssignee

if(!lastIssueInTimeRange){
log.info "No issue was found created between 16-4UTC in the last 30 days. Using the first role member"
nextAssignee = ComponentAccessor.userManager.getUserByName(userNames.first())
} else {
log.info "The last assigned issue was $lastIssueInTimeRange"
log.info "$lastIssueInTimeRange has originalAssignee=${lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField)}"
log.info "$lastIssueInTimeRange has assignee=${lastIssueInTimeRange.assignee}"
lastAssignee = lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField) ?: lastIssueInTimeRange.assignee
log.info "lastAssignee set to $lastAssignee"
def lastAssigneeIndex = userNames.indexOf(lastAssignee)
def nextAssigneeName = userNames[(lastAssigneeIndex + 1) % userNames.size()]
nextAssignee = ComponentAccessor.userManager.getUserByName(nextAssigneeName)
log.info "nextAssignee will be $nextAssignee"
}

def issueService = ComponentAccessor.issueService
def input = issueService.newIssueInputParameters()
input.setAssigneeId(nextAssignee.key)
input.addCustomFieldValue(originalAssigneeCustomField.idAsLong, nextAssignee.key)
def validationResult = issueService.validateUpdate(currentUser, issue.id, input)
assert validationResult.isValid() : validationResult.errorCollection
issueService.update(currentUser, validationResult)
 

After testing, I'm getting this error logged:

2021-07-13 21:37:51,509 ERROR [workflow.AbstractScriptWorkflowFunction]: Workflow script has failed on issue XDEFECTS-979 for user 'ibalas'. View here: https://stage.jira.vzbuilders.com/secure/admin/workflows/ViewWorkflowTransition.jspa?workflowMode=live&workflowName=ONE+Video+-+Video+Customer+Defects+7.0&descriptorTab=postfunctions&workflowTransition=1&highlight=2
java.lang.AssertionError: Errors: {customfield_20321=User 'zach.bxxxxx' was not found in the system., assignee=User 'zach.bxxxxx' does not exist.}
Error Messages: []. Expression: validationResult.isValid()
at Script391.run(Script391.groovy:79)
at com.onresolve.scriptrunner.runner.AbstractScriptRunner.runScriptAndGetContext(AbstractScriptRunner.groovy:174)
at com.onresolve.scriptrunner.runner.AbstractScriptRunner$runScriptAndGetContext$0.callCurrent(Unknown Source)
at com.onresolve.scriptrunner.runner.AbstractScriptRunner.runScriptAndGetContext(AbstractScriptRunner.groovy:293)
at com.onresolve.scriptrunner.runner.AbstractScriptRunner$runScriptAndGetContext.callCurrent(Unknown Source)
at com.onresolve.scriptrunner.runner.AbstractScriptRunner.runScript(AbstractScriptRunner.groovy:305)
at com.onresolve.scriptrunner.runner.ScriptRunner$runScript$0.call(Unknown Source)
at com.onresolve.scriptrunner.canned.jira.utils.TypedCustomScriptDelegate.execute(TypedCustomScriptDelegate.groovy:19)
at com.onresolve.scriptrunner.canned.jira.utils.TypedCustomScriptDelegate$execute$0.call(Unknown Source)
at com.onresolve.scriptrunner.canned.jira.workflow.postfunctions.CustomScriptFunction.execute(CustomScriptFunction.groovy:53)
at com.onresolve.scriptrunner.canned.jira.workflow.postfunctions.CustomScriptFunction$execute.callCurrent(Unknown Source)
at com.onresolve.scriptrunner.canned.jira.workflow.AbstractWorkflowCannedScript.execute(AbstractWorkflowCannedScript.groovy:23)
at com.onresolve.scriptrunner.canned.jira.workflow.AbstractWorkflowCannedScript$execute$1.call(Unknown Source)
at com.onresolve.scriptrunner.jira.workflow.AbstractScriptWorkflowFunction$_run_closure2.doCall(AbstractScriptWorkflowFunction.groovy:89)
at com.onresolve.scriptrunner.jira.workflow.AbstractScriptWorkflowFunction$_run_closure2.doCall(AbstractScriptWorkflowFunction.groovy)
at com.onresolve.scriptrunner.runner.diag.DiagnosticsManagerImpl$DiagnosticsExecutionHandlerImpl$_execute_closure1.doCall(DiagnosticsManagerImpl.groovy:359)
at com.onresolve.scriptrunner.runner.diag.DiagnosticsManagerImpl$DiagnosticsExecutionHandlerImpl$_execute_closure1.doCall(DiagnosticsManagerImpl.groovy)
at com.onresolve.scriptrunner.runner.ScriptExecutionRecorder.withRecording(ScriptExecutionRecorder.groovy:13)
at com.onresolve.scriptrunner.runner.ScriptExecutionRecorder$withRecording.call(Unknown Source)
at com.onresolve.scriptrunner.runner.diag.DiagnosticsManagerImpl$DiagnosticsExecutionHandlerImpl.execute(DiagnosticsManagerImpl.groovy:357)
at com.onresolve.scriptrunner.runner.diag.DiagnosticsExecutionHandler$execute$3.call(Unknown Source)
at com.onresolve.scriptrunner.jira.workflow.AbstractScriptWorkflowFunction.run(AbstractScriptWorkflowFunction.groovy:82)

I seem to be getting a validation error collection thrown claiming that customfield_20321=User 'zach.bxxxxx' was not found in the system and assignee=User 'zach.bxxxxx' does not exist. I'm not sure why I'm getting this error because the elements in the userNames array contains the user keys of the users I want included in the round robin rotation, and the syntax is correct. 

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 13, 2021

Ah ok... a bit of testing on that and I found that I had a mistaken assumption....

The InputParameter, when referring to assigneeId or value for user custom fields expects the user's name, not the user's key.

change "nextAssignee.key" to "nextAssignee.name" in the two places and it should work.

Keep in mind that the issueService is closest to the user interface. To be allowed to edit a field this way, that field must be editable from the UI as well.
If that's not what you need, I can show you a different method to edit an issue.

Ian Balas July 14, 2021

@Peter-Dave Sheehan 

Okay, so I'm not getting an error this time which is good. But still didn't get the expected result. After publishing the change to nextAssignee.name, I created a test issue expecting both the assignee and 'Original Assignee' to be set to the next user in the array. Instead, the assignee field was left unassigned and the 'Original Assignee' was set to the first user in the array..and after creating another test issue, still same result.

Also when you mention that the two fields have to be editable from the UI, do you mean I have to be able to edit the fields myself since I'm the one triggering the transition? Or they just have to be editable in general?

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
July 14, 2021

The fields must exist on the edit screen, not be hidden in the field configuration for the project and have a context for the project (in custom field configuration).

Jira doesn't have field-by-field permissions. So if the condition above are met, you and anyone else with "edit issue" permission will be able to edit those fields in the UI.

What I'm not sure... if if the fields need to be on the specific transition screen too. 

The alternative is to use the issueManager to update the issue which... come to think of it, makes more sense for the postfunction use case.

Just replace the last 7 lines with these 2:

issue.setAssignee(nextAssignee)
issue.setCustomFieldValue(originalAssigneeCustomField, nextAssignee)

 And make sure the post function is listed BEFORE the default post function called "Update change history for an issue and store the issue in the database".

Ian Balas August 2, 2021

Hi @Peter-Dave Sheehan 

My apologies again for going quiet on you. I stuck with the original iteration of this round robin script to see how it would work for a bit. While I can make do with the version that goes by project roles, still hoping to make this work where I can iterate through the array of user keys.

So to confirm regarding the context of the assignee and 'Original Assignee' fields, both are on the 'Edit Issue' screens, they're configured to the project concerned and are not hidden by any means.

I tried using the 2 lines above in place to use the issueManager as you suggested, but unfortunately still same results :( the assignee and Original Assignee fields are still defaulting to the first user in the userNames array.

Now what I thought what maybe both of use missed was there being a issueChangeHolder in place. Maybe if there's a way to refer back to the issue's change history in data store then it'll refer back to the assignee and original assignee that were set in the issue that's pulled by the jql and set to the next user in queue. But I don't think I set this correctly since I'm still getting the same results.

 

Here's what I changed:

 

...
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.user.ApplicationUser

import java.time.ZoneOffset
...

 

ApplicationUser nextAssignee

if(!lastIssueInTimeRange){
log.info "No issue was found created between 16-4UTC in the last 30 days. Using the first role member"
nextAssignee = ComponentAccessor.userManager.getUserByName(userNames.first())

def changeHolder = new DefaultIssueChangeHolder()
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, userNames.first()), changeHolder)

} else {
log.info "The last assigned issue was $lastIssueInTimeRange"
log.info "$lastIssueInTimeRange has originalAssignee=${lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField)}"
log.info "$lastIssueInTimeRange has assignee=${lastIssueInTimeRange.assignee}"
lastAssignee = lastIssueInTimeRange.getCustomFieldValue(originalAssigneeCustomField) ?: lastIssueInTimeRange.assignee
log.info "lastAssignee set to $lastAssignee"
def lastAssigneeIndex = userNames.indexOf(lastAssignee)
def nextAssigneeName = userNames[(lastAssigneeIndex + 1) % userNames.size()]
nextAssignee = ComponentAccessor.userManager.getUserByName(nextAssigneeName)
log.info "nextAssignee will be $nextAssignee"
}

issue.setAssignee(nextAssignee)
def changeHolder = new DefaultIssueChangeHolder()
originalAssigneeCustomField.updateValue(null, issue, new ModifiedValue(null, nextAssignee), changeHolder)

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
August 3, 2021

Hi @Ian Balas 

Can you share a screenshot of your transition post function screen that shows where in the sequence this script is located?

But let me go back to an earlier observation:

You might need to think of a completely different round-robin algorithm. You can't use the last assignee to know who the next one will be. You need to look at the last 2 assignees.

Was my observation correct? 

With your user array, do you need to assign to some users twice in a row before moving on to the next one? 

If so, you need a better way to keep track of where in the round robin sequence you are. Maybe this would need to happen in a separate data store somewhere.

Ian Balas August 5, 2021

@Peter-Dave Sheehan 

Sure thing, here are the screenshots:

Screen Shot 2021-08-05 at 4.43.52 PM.png

Screen Shot 2021-08-05 at 4.46.51 PM.png

And regarding your observation, originally yes. The plan was to assign to the same user twice in some instances through the round robin cycle before moving on to the next one.

But then I figured just widen the scope of users to cycle through..basically instead of assigning tickets to users twice in a row, just include a different user to take that second ticket, just to make life easier.

So instead of:

def users = ["user1","user2","user2","user3","user4","user5", "user6","user7","user8","user9","user10"]

I just added another user to the array:

def userNames = ["user1","user2","user2","user3","user4","user5", "user6","user7","user8","user9","user10", "user11"]

In the end, I want to be able to assign the users in the array above in round-robin order, similarly to Adaptavist's Round Robin example, yet not based on users in a project role, but rather users manually listed in an array within the script.

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
August 5, 2021

No, I meant a screenshot like this one:

2021-08-05 14_14_37-Transition_ Pre-Release Notification - JIRA Dev.png

The problem with your user array approach is that this line:

def lastAssigneeIndex = userNames.indexOf(lastAssignee)

When the lastAssignee is "user2", will always return index=1. 

So round robin witll start with user1, next time, it will find user1 (index=0) and move to the next user in the array (user2). The next itteration, i will find user2 (index=1) => next user = user2. The 3rd itteration, it will find user2 (index=1) again and the next it the list will be user2 again. The 4th and all subsequent itteration will always repeat the same last assignee = user2 (index=1) next assignee= user2.

That what I mean by needing to either looking at the last 2 assignees or keep track of the last assignee index separately.

For example, create a new custom field call "Round Robin Assignee Index" and update your issues each time you assign them to the index corresponding to the new assignee. Then when you get the lastIssueInTimeRange, lookup the "Round Robin Assignee Indx" instead of assignee, then use that to find the next assignee.

An alternative would be to store a custom application property:

//store a custom property
ComponentAccessor.applicationProperties.setString('customRoundRobinIndex', lastAssingeeIndex)

//retrieve a custom property
def lastAssigneeIndex = ComponentAccessor.applicationProperties.getString('customRoundRobinIndex')

With this, you don't even need to bother with searching by JQL ... just keep track of that number and each time you assign to a new person, you increase the index (or set it to zero if you've reached the max). 

 

Here is a much-simplified version:

import com.atlassian.jira.component.ComponentAccessor
import java.time.ZoneOffset

def customFieldManager = ComponentAccessor.customFieldManager

final originalAssigneeCustomFieldId = "customfield_20321"
def originalAssigneeCustomField = customFieldManager.getCustomFieldObject(originalAssigneeCustomFieldId)

// If it is true, the assigned issues will be reassigned
final reassignIssues = false

if (issue.assignee && !reassignIssues ){
//issue is already assigned, and we don't want to re-assign
return
}

def createdUtcHour = issue.created.toInstant().atOffset( ZoneOffset.ofTotalSeconds(0)).hour
if (createdUtcHour <= 4 && createdUtcHour >= 16) {
//issue was not created in the in target time range, nothing to do
return
}

def userNames = ["userkey1","userkey2","userkey3","userkey4","userkey5", "userkey6","userkey7","userkey8","userkey9","userkey10", "userkey11"]

def lastAssigneeIndex = (ComponentAccessor.applicationProperties.getString('customRoundRobinIndex') ?: 0 ) as Integer
def nextAssigneeIndex = (lastAssigneeIndex + 1) % userNames.size()
ComponentAccessor.applicationProperties.setString('customRoundRobinIndex', nextAssigneeIndex.toString())
def nextAssignee = userNames[nextAssigneeIndex]
def nextAssigneeUser = ComponentAccessor.userManager.getUserByName(nextAssignee)
issue.setCustomFieldValue(originalAssigneeCustomField, nextAssignee)
issue.setAssignee(nextAssigneeUser)
Ian Balas August 6, 2021

@Peter-Dave Sheehan 

Well I'll be! That was it. This iteration is much more simplified and the auto-assignment finally works 100% the way I expected it to work!

Here's the final piece:

 

import com.atlassian.jira.component.ComponentAccessor
import java.time.ZoneOffset

def customFieldManager = ComponentAccessor.customFieldManager

// If it is true, the assigned issues will be reassigned
final reassignIssues = false

if (issue.assignee && !reassignIssues ){
//issue is already assigned, and we don't want to re-assign
return
}

def createdUtcHour = issue.created.toInstant().atOffset(ZoneOffset.ofTotalSeconds(0)).hour
if (createdUtcHour >= 4 && createdUtcHour < 16) {
//issue was not created in the in target time range, nothing to do
return
}

def userNames = ["userkey1","userkey2","userkey3","userkey4","userkey5", "userkey6","userkey7","userkey8","userkey9","userkey10", "userkey11"]

def lastAssigneeIndex = (ComponentAccessor.applicationProperties.getString('Round Robin Assignee Index') ?: 0 ) as Integer
def nextAssigneeIndex = (lastAssigneeIndex + 1) % userNames.size()
ComponentAccessor.applicationProperties.setString('Round Robin Assignee Index', nextAssigneeIndex.toString())
def nextAssignee = userNames[nextAssigneeIndex]
def nextAssigneeUser = ComponentAccessor.userManager.getUserByName(nextAssignee)
issue.setAssignee(nextAssigneeUser)

 

As you'll probably notice, I no longer needed the 'Original Assignee' custom field to keep track of who was originally assigned to the previous ticket anymore. Even if I re-assign the auto-assigned issue, clear it out/leave blank, or even if I manually assigned the next ticket to the user who was supposed to be the next in queue...the code works in every way expected.

One final question just solely out of curiosity: how did looking into the database for a value in 'Round Robin Assignee Index' help make this work? How was it possible that the index value was stored backend even though from the user end (on the screen form itself), there's no value filled in that index field? I'm impressed that this works nonetheless.

Peter-Dave Sheehan
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
August 6, 2021

Before, you were only GUESSING at the actual index by using the last assignee.

And looking up the index from the last assignee name always returned the first matching item in the array and ignores subsequent instances of that name.

Like Ian Balas likes this
Ian Balas October 12, 2021

Hi @Peter-Dave Sheehan ,

Hope you're well. Just wanted to let you know that this post-function script has been working wonders for me ever since the last iteration you updated. As is, still works for me 100%.

There is, however, a condition that I wish to add on, if doable. I want to be able to skip over two certain users in the rotation on Fridays. 

Here's how I tried implementing that:

import com.atlassian.jira.component.ComponentAccessor
import java.sql.Timestamp
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.ZoneOffset
import java.util.Date

def customFieldManager = ComponentAccessor.customFieldManager

// If it is true, the assigned issues will be reassigned
final reassignIssues = false

if (issue.assignee && !reassignIssues ){
//issue is already assigned, and we don't want to re-assign
return
}

def createdUtcHour = issue.created.toInstant().atOffset(ZoneOffset.ofTotalSeconds(0)).hour
if (createdUtcHour >= 16 || createdUtcHour < 4) {
//issue was not created in the in target time range, nothing to do
return
}

def userNames = ["userkey1","userkey2","userkey3","userkey4",
"userkey5", "userkey6","userkey7","userkey8","userkey9","userkey10",
"userkey11"]

def lastAssigneeIndex = (ComponentAccessor.applicationProperties.getString('Round Robin Assignee Index:') ?: 0 ) as Integer
def nextAssigneeIndex = (lastAssigneeIndex + 1) % userNames.size()
def nextAssigneeAfterIndex = (lastAssigneeIndex + 2) % userNames.size()
ComponentAccessor.applicationProperties.setString('Round Robin Assignee Index:', nextAssigneeIndex.toString())
def nextAssignee = userNames[nextAssigneeIndex]
def nextAssigneeAfter = userNames[nextAssigneeAfterIndex]
def nextAssigneeUser = ComponentAccessor.userManager.getUserByName(nextAssignee)
def nextAssigneeAfterUser = ComponentAccessor.userManager.getUserByName(nextAssigneeAfter)

// Get day of week
def today = new Date().format('yyyy-MM-dd');
def weekday = Date.parse('yyyy-MM-dd', today)[Calendar.DAY_OF_WEEK];


if (weekday == Calendar.FRIDAY && (lastAssigneeIndex == 4 || lastAssigneeIndex == 11) ) {
issue.setAssignee(nextAssigneeAfterUser)
} else {
issue.setAssignee(nextAssigneeUser)
}

The users denoted by userkey6 and userkey11 are the users that are to be skipped over on Fridays. 

I tried testing this by setting the weekday value to TUESDAY (today) instead of FRIDAY and creating a test issue. In this case, the previous issue was auto-assigned to userkey10 so it should have skipped over and assigned to userkey12, but it didn't work and it still assigned to userkey11.

Would you happen to know where I went wrong with editing the script?

Ian Balas October 13, 2021

@Peter-Dave Sheehan 

Never mind, was able to figure it out. I didn't set the index in the database itself, which it was pulling from. Here's how I edited it once more, and I can confirm that this works:

import com.atlassian.jira.component.ComponentAccessor
import java.sql.Timestamp
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.ZoneOffset
import java.util.Date

def customFieldManager = ComponentAccessor.customFieldManager

// If it is true, the assigned issues will be reassigned
final reassignIssues = false

if (issue.assignee && !reassignIssues ){
//issue is already assigned, and we don't want to re-assign
return
}

def createdUtcHour = issue.created.toInstant().atOffset(ZoneOffset.ofTotalSeconds(0)).hour
if (createdUtcHour >= 16 || createdUtcHour < 4) {
//issue was not created in the in target time range, nothing to do
return
}

def userNames = ["userkey1","userkey2","userkey3","userkey4",
"userkey5", "userkey6","userkey7","userkey8","userkey9","userkey10",
"userkey11"]

def lastAssigneeIndex = (ComponentAccessor.applicationProperties.getString('Round Robin Assignee Index:') ?: 0 ) as Integer
def nextAssigneeIndex = (lastAssigneeIndex + 1) % userNames.size()
ComponentAccessor.applicationProperties.setString('Round Robin Assignee Index:', nextAssigneeIndex.toString())
def nextAssignee = userNames[nextAssigneeIndex]
def nextAssigneeUser = ComponentAccessor.userManager.getUserByName(nextAssignee)

// Get day of week (For IL user exception)
def today = new Date().format('yyyy-MM-dd');
def weekday = Date.parse('yyyy-MM-dd', today)[Calendar.DAY_OF_WEEK];

if (weekday == Calendar.FRIDAY && (lastAssigneeIndex == 4 || lastAssigneeIndex == 11) ) {

def nextAssigneeIndex2 = (lastAssigneeIndex + 2) % userNames.size()
def nextAssignee2 = userNames[nextAssigneeIndex2]
def nextAssigneeUser2 = ComponentAccessor.userManager.getUserByName(nextAssignee2)
ComponentAccessor.applicationProperties.setString('Round Robin Assignee Index:', nextAssigneeIndex2.toString())
issue.setAssignee(nextAssigneeUser2)

} else if (weekday == Calendar.FRIDAY && lastAssigneeIndex == 10) {

def nextAssigneeIndex3 = (lastAssigneeIndex + 3) % userNames.size()
def nextAssignee3 = userNames[nextAssigneeIndex3]
def nextAssigneeUser3 = ComponentAccessor.userManager.getUserByName(nextAssignee3)
ComponentAccessor.applicationProperties.setString('Round Robin Assignee Index:', nextAssigneeIndex3.toString())
issue.setAssignee(nextAssigneeUser3)

} else {

issue.setAssignee(nextAssigneeUser)

}  

Suggest an answer

Log in or Sign up to answer