Reverse issue link direction

Hi there,
as part of migrating data to our Jira, we encountered the annoying limitation that Jira treats all issue links in a CSV import as outgoing links. While we could have fiddled with the data to work around this, we instead solved this with Scriptrunner:

// Reverse link direction of all issue links of a given issue link type for a given issue
import groovy.xml.MarkupBuilder
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys
import com.atlassian.jira.issue.link.IssueLinkTypeManager

import com.atlassian.jira.issue.link.IssueLink
import com.atlassian.jira.issue.link.IssueLinkType
import com.onresolve.scriptrunner.parameters.annotation.IssueLinkTypePicker
import com.onresolve.scriptrunner.parameters.annotation.*

// Get the components
def issueManager = ComponentAccessor.issueManager
def issueLinkManager = ComponentAccessor.issueLinkManager

def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issueLinkTypeManager = ComponentAccessor.getComponent(IssueLinkTypeManager)

@ShortTextInput(label = "Issue key", description = "Enter a issue key")
String issueKey

@IssueLinkTypePicker(label = 'Issue link type', description = 'Pick an issue link type', placeholder = 'Pick an issue link type')
IssueLinkType issueLinkType

@Checkbox(label = "Reverse inwards links", description = "Check to reverse inwards links")
Boolean inwardLinksShouldBeReversed
@Checkbox(label = "Reverse outwards links", description = "Check to reverse outwards links")
Boolean outwardLinksShouldBeReversed

@Checkbox(label = "Preview", description = "Check for dry run. If Checked, nothing will actually be changed")
Boolean preview

// Get the issue
def issue = issueManager.getIssueByCurrentKey(issueKey)
assert issue : "no issue found for '${issueKey}'"

// Get the issue links to other issues
def inwardLinks = issueLinkManager.getInwardLinks(issue.id).findAll { it.issueLinkType == issueLinkType }
def outwardLinks = issueLinkManager.getOutwardLinks(issue.id).findAll { it.issueLinkType == issueLinkType }
final Long sequence = 1L


def writer = new StringWriter()
def builder = new MarkupBuilder(writer)
builder.style(type: "text/css",
    '''
         #output, #output td, #output th{
                border: 1px solid black;
                padding: 1em;
               
            }
            #output{
                border-collapse: collapse;
            }
            th {
            	background: lightgray;
            }
        ''') 
builder.p {    
    p "${inwardLinks.size()} incoming links and ${outwardLinks.size()} outgoing links found for issue ${issueKey} and link type ${issueLinkType.name}."
    p "Inward links will ${inwardLinksShouldBeReversed   ? '': 'not'} be reversed"
    p "Outward links will ${outwardLinksShouldBeReversed ? '': 'not'} be reversed"
    p { i "${preview ? 'DRY RUN - nothing will be changed': 'changes will be applied'}" }

    table(id: "output") {
        tr {
            th "Original link"
            th "new reversed link"            
        }

        // reverse link direction for inwards links
        if (inwardLinksShouldBeReversed) {
            def newSourceIssue = issue
            inwardLinks.each { IssueLink link ->
                def newDestinationIssue = link.sourceObject
                tr {
                    td { // Original link
                        a href: urlify(link.destinationObject.key), link.destinationObject.key
                        i link.getIssueLinkType().getInward()
                        a href: urlify(link.sourceObject.key), link.sourceObject.key
                    } 
					td { // new reversed link
                        a href: urlify(newDestinationIssue.key), newDestinationIssue.key
                        i issueLinkType.getInward()
                        a href: urlify(newSourceIssue.key), newSourceIssue.key
                    } 
                }
                if (preview == false) {
                    log.warn "reversing existing link '${link.sourceObject.key} ${link.getIssueLinkType().getOutward()} ${link.destinationObject.key}'"
                    issueLinkManager.createIssueLink(newSourceIssue.id, newDestinationIssue.id, issueLinkType.id, sequence, loggedInUser)
                    issueLinkManager.removeIssueLink(link, loggedInUser)
                    
                }
			}
        }
        // reverse link direction for outwards links
        if (outwardLinksShouldBeReversed) {
            def newDestinationIssue = issue
            outwardLinks.each { IssueLink link ->
                def newSourceIssue = link.destinationObject
                tr {
                    td { // Original link
                        a href: urlify(link.sourceObject.key), link.sourceObject.key
                        i link.getIssueLinkType().getOutward()
                        a href: urlify(link.destinationObject.key), link.destinationObject.key
                    } 
					td { // new reversed link
                        a href: urlify(newSourceIssue.key), newSourceIssue.key
                        i issueLinkType.getOutward()
                        a href: urlify(newDestinationIssue.key), newDestinationIssue.key
                    } 
                }
                
                if (preview == false) {
                    issueLinkManager.createIssueLink(newSourceIssue.id, newDestinationIssue.id, issueLinkType.id, sequence, loggedInUser)
                    issueLinkManager.removeIssueLink(link, loggedInUser)
                    log.warn "reversing existing link '${link.sourceObject.key} ${link.getIssueLinkType().getOutward()} ${link.destinationObject.key}'"
                }
            }
        }
    }
}

return writer.toString()

def urlify(issuekey) {
	def baseUrl = ComponentAccessor.applicationProperties.getString(APKeys.JIRA_BASEURL)
	return "${baseUrl}/browse/${issuekey}"
}

The script will allow to select a link type, a source issue and link directions as inputs. It will then delete the applicable issue links and replace them with links of the same type with reverse direction.
It outputs the results in a nice table.
Currently for a single issue, but could easily be modified to have a list of issues or JQL search as input.

1 Like