Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Automatically linking issues with Scriptrunner for Jira Cloud

Tamir Mubarak May 31, 2023

Hello dear Community, 

currently I have following requirements
As a User, I want that all my issues automatically get a parent/child relationship when I link two tickets together (and vice versa, but this requirements is negligible for now), so that my child issues always have the Epic Link. 

AC01: The Script should only run if an Epic is linked to a child (Story, Feature) 

AC02: The Script Listener should run as soon as I´m creating a link between to tickets

AC03: The Script Listener should run as soon as I´m creating a Parent/Child relationship

AC04: The Script should run in a "loop" so that all linked issues get the Epic Link (Parent/Child relationship). I already have a script which works as I want, but it only uses the first linked issue.

I wrote with the help of a colleague this script and let it run in the script console. It works perfectly, except of the fact, that it only uses the first linked issue in the epic and not all linked issues...

import groovy.json.JsonOutput

def sec = get("/rest/api/2/issue/AK-3")
.header("Content-Type", "application/json")
.asObject(Map)
.body
sec

def tick = sec.fields.issuelinks.inwardIssue[0].key

def returnData = [
fields: [
"parent": sec
]
]

def updatedIssue = put("/rest/api/2/issue/${tick}")
.header("Content-Type", "application/json")
.body(JsonOutput.toJson(returnData))
.asString()

updatedIssue

 

I tried to recreate this script in the Script Listener, but unfortunately it doesn't work: 

import groovy.json.JsonOutput

def source = issueLink.sourceIssueId
def dest = issueLink.destinationIssueId

def issue = get("/rest/api/2/issue/${source}")
.header("Content-Type", "application/json")
.asObject(Map)
.body

def returnData = [
fields: [
"parent": issue
]
]

def updatedIssue = put("/rest/api/2/issue/${dest}")
.header("Content-Type", "application/json")
.body(JsonOutput.toJson(returnData))
.asString()

updatedIssue

 

I would love to hear your suggestions and thanks in advance for helping me out. 

Best regards, 

Tamir 

3 answers

1 accepted

0 votes
Answer accepted
Bobby Bailey
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.
June 2, 2023

@Tamir Mubarak glad to hear you were able to get the console script working as you needed!

As for your Listener script, I was wondering why that didn't work, as the code you had written there was correct, but on reflection, I think I understand what's missing. 

Just to clarify what you need (to confirm that I understand) when you update the Epic Link field on an issue (let's call this Issue A), you want the script to get all issues linked to Issue A, and update their Epic Link field with the epic that Issue A was linked to.

If this is correct, read on, if not then please let me know where I got it wrong!

So, assuming that is correct, you do want an event listener that activates on an issue link. I have written this code and added comments, but will describe how it works below for clarity:

import groovy.json.JsonOutput

def issueLinkType = issueLink.issueLinkType.name
logger.info('Issue Link Type : '+issueLinkType)

// First lets check that we are dealing with the correct link type
if(issueLinkType == 'Epic-Story Link'){
def source = issueLink.sourceIssueId
def dest = issueLink.destinationIssueId

//get the Epic issue key
def epicIssue = get("/rest/api/2/issue/${source}")
.header("Content-Type", "application/json")
.asObject(Map)
.body

def epicIssueKey = epicIssue.key

// get the Story Issue Key and Links
def storyIssue = get("/rest/api/2/issue/${dest}")
.header("Content-Type", "application/json")
.asObject(Map)
.body

def storyIssueKey = storyIssue.key
def storyIssueLinks = storyIssue.fields.issuelinks

logger.info('Epic Key : '+epicIssueKey)
logger.info('Story Key : '+storyIssueKey)

// Loop through all issue links
storyIssueLinks.each { issueLink ->

// Here, as we are assigning an epic, instead of adding the epic as a parent, we want to assign the epic key to the epic link field
// Ensure the custom field ID is the correct id for the Epic Link Field
def returnData = [
fields: [
customfield_10014: epicIssueKey
]
]

// get the linked issue, which could be an inward or outward issue. We check to see which is empty, as the other will be the the issue
def inwardIssue = issueLink.inwardIssue
def outwardIssue = issueLink.outwardIssue
def issueToUpdate = (inwardIssue == null) ? outwardIssue : inwardIssue
def issueToUpdateKey = issueToUpdate.key

logger.info('Link Inward issue : '+inwardIssue.toString())
logger.info('Link Outward issue : '+outwardIssue.toString())
logger.info('Issue to update : ' + issueToUpdate.toString())
logger.info('Issue Key to update : ' +issueToUpdateKey)

// Update the linked issue with the correct epic link
def updatedIssue = put("/rest/api/2/issue/${issueToUpdateKey}")
.header("Content-Type", "application/json")
.body(JsonOutput.toJson(returnData))
.asString()

logger.info("Response Body: " + updatedIssue.body)
}
}

Firstly, we want to check which link type it is, as we are only interested in a specific type of link (you may need to expand this based on your needs)

Next, let's get the relevant issues keys for ease of use. 

Then we get the Issues linked to the child/story issue and loop through them. 

For each, we find out the linked issue (whether that be an inward or outward link), and update the Epic Link custom field with the key of the Epic. 

Things to be aware of are that your custom field ID needs to match the Epic Link custom field ID. Also, I have left my logging code in there for you to test and make changes in case it doesn't exactly meet your needs. 

I hope this helps!


Tamir Mubarak June 2, 2023

wow Robert thank you very much!! 

I really appreciate your effort. 

The code doesn't fit 100% my requirements, but still I'll use this code, because this functionality is very helpful. 

What I need: 

When I create a link between an Epic and a story, I want the script to get all issues linked to the Epic, and update the Epic Link field of the linked stories with the epic where they linked to.

In short: the outcome should be that every story which is linked to an epic, also has a Parent/Child-Relationship to the linked epic.

Bobby Bailey
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.
June 2, 2023

Ahh, apologies for the misunderstanding!

That might require a little more consideration. The issue with that concept is that the logic ends up being recursive. In essence you will need to write a script that checks to see if the link includes an epic, and if it does, go and perform your logic. The problem with that is that your code will check, and if there are any missing create the Epic Link, which in itself is a new link, which includes an epic, which will trigger your script. 

Fortunately, this recursion would only go to one level, as the first run through will create all links needed, and your additional executions would find no new links need to be created so don't spawn any more events. That being said, depending on the amount of data you do run the risk of spamming your instance/listener/logs with lots of inefficient executions. 

I would personally recommend two scripts. I would create a console script that cleans up historic data, it would look something like this: 

import groovy.json.JsonOutput

// The JQL query to search the issues by
final jqlQuery = "issueType = Epic"

// Get all issues matching the specified JQL Query
def allEpics = get("/rest/api/3/search")
.queryString("jql", jqlQuery)
.asObject(Map)
.body
.issues as List<Map>

allEpics.each{ issue ->

//Get the Epic key
def epicKey = issue.key

//Get the linked issues
def epicLinkedIssues = issue.fields.issuelinks

//loop through the issues and update the Epic Link field
def returnData = [
fields: [
customfield_10014: epicKey
]
]

epicLinkedIssues.each{ issueLink ->
def inwardIssue = issueLink.inwardIssue
def outwardIssue = issueLink.outwardIssue
def issueToUpdate = (inwardIssue == null) ? outwardIssue : inwardIssue
def issueToUpdateKey = issueToUpdate.key

def updatedIssue = put("/rest/api/3/issue/${issueToUpdateKey}")
.header("Content-Type", "application/json")
.body(JsonOutput.toJson(returnData))
.asString()
}
}

 

There are two main things to consider, the first is that this will override any historical data that does not match your existing logic. You can prevent that by either modifying the JQL or adding more code that checks the issues Epic Link value before overriding it. 

The second is the script timeout limit. If you have a large amount of data then you will likely hit this. If you do, I would recommend breaking this down into chunks (again using the JQL)

Once your data is at a point where it matches your logic, then you only need to ensure the Epic Link field is kept in sync with the issue link that was created via a Listener on Issue Link created:

import groovy.json.JsonOutput

def issueLinkType = issueLink.issueLinkType.name

def source = issueLink.sourceIssueId
def dest = issueLink.destinationIssueId

//Get the issue type for both sides of the link
def sourceIssue = get("/rest/api/3/issue/${source}")
.header("Content-Type", "application/json")
.asObject(Map)
.body

def destIssue = get("/rest/api/3/issue/${dest}")
.header("Content-Type", "application/json")
.asObject(Map)
.body

def sourceIssueType = sourceIssue.fields.issuetype.name
def destIssueType = destIssue.fields.issuetype.name

logger.info(sourceIssueType)
logger.info(destIssueType)

// If either side of the link is an Epic, then we want to continue, but ignore Epic Link creation

if((sourceIssueType == "Epic" || destIssueType == "Epic") && issueLinkType != 'Epic-Story Link') {
// figure out which issue is which
def epicIssue = (sourceIssueType == "Epic") ? sourceIssue : destIssue
def childIssue = (sourceIssueType != "Epic") ? sourceIssue : destIssue

if(epicIssue == null || childIssue == null){
// Saftey check incase someone links two Epics together, this code assumes the link was Epic<-> non-Epic
return
}

def returnData = [
fields: [
customfield_10014: epicIssue.key
]
]

def updatedIssue = put("/rest/api/2/issue/${childIssue.key}")
.header("Content-Type", "application/json")
.body(JsonOutput.toJson(returnData))
.asString()
}




This does fire two events, but there isn't much more you can do other than the checks included to make sure the second doesn't perform any actions. 

I hope this all makes sense! As always, please review and test this code before using it. 

Let me know if you have any other questions!

Tamir Mubarak June 3, 2023

Hello Robert,

 

thank you so much for your effort. This will solve my problems! :) 

 

Best regards, 

Tamir 

1 vote
Bobby Bailey
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.
June 2, 2023

Hi @Tamir Mubarak

In your console script,

sec.fields.issuelinks

is returning an array, however, you are not handling it like an array. If you try:

 

def secIssueLinks = sec.fields.issuelinks

secIssueLinks.each{
logger.info(it.id)
}

 

You will see all of the issue link ids and can process them then. I haven't expanded the code out, as I am not 100% sure of the logic you need to follow, but this is how you can access all issue links. 

As for your Listener script, could you provide more details about what is not working?

Thanks!

Bobby -- ScriptRunner Customer Success Manager

0 votes
Tamir Mubarak June 2, 2023

Hello @Bobby Bailey

thank you a lot for your suggestion and your help! 

I adapted the code as followed and it now processes all the linked issues and not just the first one. 

import groovy.json.JsonOutput

def sec = get("/rest/api/2/issue/AK-4")
    .header("Content-Type", "application/json")
    .asObject(Map)
    .body

def secIssueLinks = sec.fields.issuelinks

secIssueLinks.each { issueLink ->
    logger.info(issueLink.id)

    def returnData = [
        fields: [
            "parent": sec
        ]
    ]

    def tick = issueLink.inwardIssue.key

    def updatedIssue = put("/rest/api/2/issue/${tick}")

        .header("Content-Type", "application/json")

        .body(JsonOutput.toJson(returnData))

        .asString()

    logger.info("Response Body: " + updatedIssue.body)

}

I got these error messages, but the code is still working:
[Static type checking] - No such property: issuelinks for class: java.lang.Object @line 8, column 21 [Static type checking] - No such property: id for class: java.lang.object @line 11, column 17 [Static type checking] - cannot find matching method org.slf4j.logger#info(java.lang.object) @line11, column 5 [Static type checking] - No such property: inwardIssue for class: java.lang.object @line 19, column 16

I tried to adapt the code for the Script Listener, because I just need this script when a link between two issues is created. Following you can see my script:

import groovy.json.JsonOutput

def sec = get("/rest/api/2/issue/${event.issue.key}")
.header("Content-Type", "application/json")
.asObject(Map)
.body

def secIssueLinks = sec.fields.issuelinks

secIssueLinks.each { issueLink ->
logger.info(issueLink.id)

def returnData = [
fields: [
"parent": sec
]
]

def tick = issueLink.inwardIssue.key

def updatedIssue = put("/rest/api/2/issue/${tick}")
.header("Content-Type", "application/json")
.body(JsonOutput.toJson(returnData))
.asString()

logger.info("Response Body: " + updatedIssue.body)

Could you please help me to adapt the code for the script listener? 

The trigger event, is as I told you, the linking between two issues (epic and subordinate issue). Could the problem be that there is no "event.issue" when two tickets are linked?

What would be the alternative? Maybe a scheduled job?

Thank you in advance!

Best regards,

Tamir  

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
CLOUD
PRODUCT PLAN
STANDARD
PERMISSIONS LEVEL
Product Admin
TAGS
AUG Leaders

Atlassian Community Events