ScriptRunner Listener: Get sprint end date and set it as due date of the issue

Reno February 24, 2022

Hello,

What I am trying to do is simple, and I had a partial success following other posts but not yet completely.

I want to create a ScriptRunner Listener custom script that executes every time an issue is updated, if the issue is associated with a sprint, take its end date and set it as the due date of the issue.

So far I achieved to get the code to set the due date of the issue, but I need to get the sprint end date in  "dd/MMM/aa" format to work with this code (according to the documentation of the method here):

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.bc.issue.IssueService

def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issue = event.issue
IssueService issueService = ComponentAccessor.getIssueService()

// Get sprint end date
def SprintEndDate = ¿?¿?¿?

// Set Due Date
def issueInputParameters = issueService.newIssueInputParameters()
issueInputParameters.setDueDate(SprintEndDate)

def updateValidationResult = issueService.validateUpdate(loggedInUser, issue.id, issueInputParameters)
if (updateValidationResult.isValid()) {
// Validate the update
def issueResult = issueService.update(loggedInUser, updateValidationResult)

 I also got the part to get the Sprint End Date like this:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.issue.ModifiedValue
import java.sql.Timestamp;

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def sprint = customFieldManager.getCustomFieldObjectByName("Sprint")

//End Date
Date endDate = event.issue.getCustomFieldValue(sprint)?.endDate?.first()?.toDate()

But I am having trouble merging both parts.

Can anyone help me?

Thank you so much in advance.

1 answer

1 accepted

Suggest an answer

Log in or Sign up to answer
2 votes
Answer accepted
Peter-Dave Sheehan
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
February 24, 2022

Have a look at this script that combines both concepts along with an additional trick of only running when the change contains an actual update to the sprint field.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.greenhopper.service.sprint.Sprint
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
@WithPlugin("com.pyxis.greenhopper.jira")

def issueService = ComponentAccessor.issueService
def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def event = event as IssueEvent //re-cast to help with static checking error in the Script Editor. Don't need this if you edit your script in line
def issue = event.issue

def changeItems = ComponentAccessor.changeHistoryManager.getChangeHistoryById(event.changeLog.id as Long).changeItemBeans
if (!changeItems.any { it.field == 'Sprint' }) {
//skip this listener if the sprint field wasn't changed
return
}

def sprintCf = ComponentAccessor.customFieldManager.getCustomFieldObjectsByName("Sprint")[0]
//get the current list of sprints on the issue
def sprints = issue.getCustomFieldValue(sprintCf) as List<Sprint>
if (!sprints) {
//if sprint field is empty, the change was to delete the last remaining sprint on the issue.
// You might choose to clear the due date here
return
}

//using last sprint. If there are more than one, the last one is most likely to be active the others should be closed
def sprint = sprints.last()
//convert from default dateTime to Date class so we can use the format method later
def endDate = sprint.endDate.toDate()
// Set Due Date
def issueInputParameters = issueService.newIssueInputParameters()
issueInputParameters.setDueDate(endDate.format('dd/MMM/yyyy'))
def updateValidationResult = issueService.validateUpdate(loggedInUser, issue.id, issueInputParameters)
if (!updateValidationResult.isValid()) {
log.error "Unable to edit the due date to $endDate. Error: $updateValidationResult.errorCollection"
return
}
def issueResult = issueService.update(loggedInUser, updateValidationResult)
Reno February 25, 2022

Thank you Peter!

This helped me greatly.

I also added a slight modification to be able to keep the selected due date (if any) during the 'Issue Created' event: 

 

// Get the event type
def eventTypeManager = ComponentAccessor.getEventTypeManager()
def eventTypeName = eventTypeManager.getEventType(event.eventTypeId).getName()
if (eventTypeName == "Issue Created") {
    def DueDate = issue.getDueDate()
    if (DueDate) {
        // Skip this listener if the due date is set on creation
        return
    }
} else {
    def changeItems = ComponentAccessor.changeHistoryManager.getChangeHistoryById(event.changeLog.id as Long).changeItemBeans
    if (!changeItems.any { it.field == 'Sprint' }) {
        // Skip this listener if the sprint field wasn't changed
        return
    }
}
Reno February 25, 2022

The complete listener behaviour is:

  • On issue creation:
    • Due date not filled, a sprint is selected. -> The due date will automatically be set as the selected sprint's end date.
    • Due date filled, a sprint is selected. -> The selected due date will be kept, ignoring the selected sprint's end date.
  • On issue's sprint modification:
    • The due date will automatically be set as the selected sprint's end date.
    • If the due date is changed afterwards, but not the sprint, the new due date will be kept.
    • If the due date has changed, and the sprint is changed, the due date will be overwritten with the selected sprint's end date.
Reno February 25, 2022

I thought that maybe one may want to update the subtasks' due date when the parent is changed for the previous reasons. I'll leave what I added in case it helps anybody:

// Set Due Date of sub-tasks (if any)
if (issue.isSubTask()) {
    return
} else {
    Collection<Issue> subTasksCol = issue.getSubTaskObjects();
    // Tterate colleaction with subtasks and update duedate of each of them
    for (Issue subTaskEl : subTasksCol) {
        def subupdateValidationResult = issueService.validateUpdate(loggedInUser, subTaskEl.id, issueInputParameters)
        if (!subupdateValidationResult.isValid()) {
            log.error "Unable to edit the due date to $endDate. Error: $subupdateValidationResult.errorCollection"
            return
        }
        def subissueResult = issueService.update(loggedInUser, subupdateValidationResult)
    }
}
Peter-Dave Sheehan
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
February 25, 2022

Here is a shortcut you might appreciate to filter based on event type rather than getting and using the eventTypeManager

import com.atlassian.jira.event.type.EventType

if( event.eventTypeId == EventType.ISSUE_CREATED_ID && event.issue.dueDate){
// Skip this listener if the due date is set on creation
return
}

You still get code that's easy to read and understand.

You don't need to extract the due date from the issue, you can check that right in the if statement. This will return a groovy truthy value. 

Like Reno likes this
Reno February 28, 2022

Thank you Peter, that is definitely a much cleaner code!

Marc Colangelo
Contributor
March 31, 2022

Hi All!

 

This is something I am trying to implement in our Jira Server instance.

I started with just copying the original from @Peter-Dave Sheehan with the combined code just to see what would occur.

This fails. Logs and payload showing below.

Error points to "IssueEvent" which was created in the event = event as IssueEvent code snippet. 

def event = event as IssueEvent //re-cast to help with static checking error in the Script Editor. Don't need this if you edit your script in line

 

  • Do either of you have this working?
  • What was the final code used?

 

Code

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.greenhopper.service.sprint.Sprint
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
@WithPlugin("com.pyxis.greenhopper.jira")

def issueService = ComponentAccessor.issueService
def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def event = event as IssueEvent //re-cast to help with static checking error in the Script Editor. Don't need this if you edit your script in line
def issue = event.issue

def changeItems = ComponentAccessor.changeHistoryManager.getChangeHistoryById(event.changeLog.id as Long).changeItemBeans
if (!changeItems.any { it.field == 'Sprint' }) {
//skip this listener if the sprint field wasn't changed
return
}

def sprintCf = ComponentAccessor.customFieldManager.getCustomFieldObjectsByName("Sprint")[0]
//get the current list of sprints on the issue
def sprints = issue.getCustomFieldValue(sprintCf) as List<Sprint>
if (!sprints) {
//if sprint field is empty, the change was to delete the last remaining sprint on the issue.
// You might choose to clear the due date here
return
}

//using last sprint. If there are more than one, the last one is most likely to be active the others should be closed
def sprint = sprints.last()
//convert from default dateTime to Date class so we can use the format method later
def endDate = sprint.endDate.toDate()
// Set Due Date
def issueInputParameters = issueService.newIssueInputParameters()
issueInputParameters.setDueDate(endDate.format('dd/MMM/yyyy'))
def updateValidationResult = issueService.validateUpdate(loggedInUser, issue.id, issueInputParameters)
if (!updateValidationResult.isValid()) {
log.error "Unable to edit the due date to $endDate. Error: $updateValidationResult.errorCollection"
return
}
def issueResult = issueService.update(loggedInUser, updateValidationResult)

 

Log

2022-03-31 16:46:25,661 ERROR [runner.AbstractScriptListener]: *************************************************************************************
2022-03-31 16:46:25,667 ERROR [runner.AbstractScriptListener]: Script function failed on event: com.atlassian.greenhopper.api.events.sprint.SprintUpdatedEvent, file: null
org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'com.atlassian.greenhopper.api.events.sprint.SprintUpdatedEvent@1162' with class 'com.atlassian.greenhopper.api.events.sprint.SprintUpdatedEvent' to class 'com.atlassian.jira.event.issue.IssueEvent'
at Script2.run(Script2.groovy:9)

 

Payload

{  "projects": "[VI3] (java.util.ArrayList)",  "@class": "com.onresolve.scriptrunner.canned.jira.workflow.listeners.model.CustomListenerCommand (java.lang.String)",  "log": "org.apache.log4j.Logger@4ea9f02a",  "friendlyEventNames": "Issue Created, SprintUpdatedEvent (java.lang.String)",  "version": "7 (java.lang.Integer)",  "relatedProjects": "[[key:VI3, name:Vi3 - TEST]] (java.util.ArrayList)",  "name": "Custom listener (java.lang.String)",  "canned-script": "com.onresolve.scriptrunner.canned.jira.workflow.listeners.CustomListener (java.lang.String)",  "disabled": "false (java.lang.Boolean)",  "id": "8f5afdd3-08c1-4b04-9db0-3e63b1b791e0 (java.lang.String)",  "event": "com.atlassian.greenhopper.api.events.sprint.SprintUpdatedEvent@1162",  "\u00a3beanContext": "com.onresolve.scriptrunner.beans.BeanFactoryBackedBeanContext@5b6d03eb",  "events": "[1, com.atlassian.greenhopper.api.events.sprint.SprintUpdatedEvent] (java.util.ArrayList)"
Peter-Dave Sheehan
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
March 31, 2022

The code I suggested was for a listener that was configured to listen to the "Issue Updated" event (so you can detect when an issue is assigned to a sprint).

If you are attempting to listen to the "Sprint Updated" event, the following line won't work:

def event = event as IssueEvent 

Neither will 

def issue = event.issue

Since I don't think Sprint events contain an issue object.

Marc Colangelo
Contributor
March 31, 2022

Hi @Peter-Dave Sheehan , thank you for that detail.

I was certainly confused by the events.

I updated this on my end and now I get the basic function of:

  • When sprint is created and has end date, then issue is added to sprint, DUE DATE is updated on the Issue to match the end date of the sprint

 

I will look to incorporate those other changes as the cases outlined by @Reno are the behavior I would expect also.

 

Thank you!!!!

Marc Colangelo
Contributor
March 31, 2022

Hi @Reno 

I see you note the completed behaviors for the listener and I may have made some mistakes in my code.

The behavior I am trying to solve for:

  • Sprint is created, end date set.
    • Issue is added, due date is set using sprint end date.
    • Sprint end date is then updated, issue due date is updated

End result is issue's due date stays in sync with the sprint end date.

 

Currently, I have to move an issue out of the sprint and then back in to have it adjusted or I have to make the manual update.

 

Any advice is appreciated!

Reno April 1, 2022

Hi @Marc Colangelo

As I have it working now, it does not update the due date of the issue if the end date of the sprint is changed.

Since this is something that does not happen in my projects, and the sprints are fixed in time, I am not bothered, though it is a nice to have feature.

Also, find below my final code working as I descrived:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.event.type.EventType
import com.atlassian.greenhopper.service.sprint.Sprint
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
@WithPlugin("com.pyxis.greenhopper.jira")

def issueService = ComponentAccessor.issueService
def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def event = event as IssueEvent // Re-cast to help with static checking error in the Script Editor. Not needed when editing the script in line.
def issue = event.issue

// Get the event type
if(event.eventTypeId == EventType.ISSUE_CREATED_ID){
    if(event.issue.dueDate) {
        // Skip this listener if the due date is set on creation
        return
    }
} else {
    def changeItems = ComponentAccessor.changeHistoryManager.getChangeHistoryById(event.changeLog.id as Long).changeItemBeans
    if (!changeItems.any { it.field == 'Sprint' }) {
        // Skip this listener if the sprint field wasn't changed
        return
    }
}

def sprintCf = ComponentAccessor.customFieldManager.getCustomFieldObjectsByName("Sprint")[0]

// Get the current list of sprints on the issue
def sprints = issue.getCustomFieldValue(sprintCf) as List<Sprint>
if (!sprints) {
    // If sprint field is empty, the change was to delete the last remaining sprint on the issue.
    // We might choose to clear the due date here
    return
}

// Using the last sprint. If there are more than one, the last one is most likely to be active, the others should be closed.
def sprint = sprints.last()

// Convert from default dateTime to Date class so we can use the format method later
def endDate = sprint.endDate.toDate()

// Set Due Date
def issueInputParameters = issueService.newIssueInputParameters()
issueInputParameters.setDueDate(endDate.format('dd/MMM/yyyy'))
def updateValidationResult = issueService.validateUpdate(loggedInUser, issue.id, issueInputParameters)

if (!updateValidationResult.isValid()) {
    log.error "Unable to edit the due date to $endDate. Error: $updateValidationResult.errorCollection"
    return
}

def issueResult = issueService.update(loggedInUser, updateValidationResult)
Marc Colangelo
Contributor
April 1, 2022

Thank you, @Reno

 

Agreed that once a sprint starts, the date does not change.  My company does have cases where we plan our sprints but business needs take over and then the update to a sprints end date occurs prior to the start of a sprint.

 

Appreciate the support and the code!

Reno April 1, 2022

I am glad to help! :)

Uma Chaudhary
I'm New Here
I'm New Here
Those new to the Atlassian Community have posted less than three times. Give them a warm welcome!
June 3, 2022

need to edit sprit date in cloud jira have permissions but disabled june 6 - june 16 needed? right now have june 6 to june 14

TAGS
AUG Leaders

Atlassian Community Events