Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in
Celebration

Earn badges and make progress

You're on your way to the next level! Join the Kudos program to earn points and save your progress.

Deleted user Avatar
Deleted user

Level 1: Seed

25 / 150 points

Next: Root

Avatar

1 badge earned

Collect

Participate in fun challenges

Challenges come and go, but your rewards stay with you. Do more to earn more!

Challenges
Coins

Gift kudos to your peers

What goes around comes around! Share the love by gifting kudos to your peers.

Recognition
Ribbon

Rise up in the ranks

Keep earning points to reach the top of the leaderboard. It resets every quarter so you always have a chance!

Leaderboard

Come for the products,
stay for the community

The Atlassian Community can help you and your team get more value out of Atlassian products and practices.

Atlassian Community about banner
4,459,382
Community Members
 
Community Events
176
Community Groups

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

Edited

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

2 votes
Answer accepted

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)

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
    }
}

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.

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)
    }
}

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

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

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)"

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.

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!!!!

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!

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)

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!

I am glad to help! :)

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

Suggest an answer

Log in or Sign up to answer
TAGS

Atlassian Community Events