Checking for an existing issue clone inside another project

Juan Felipe Garcia Carreño January 24, 2023

Hi everyone,

We are cloning an issue from one project to another using a listener (the code is below). The first part of it checks if there is already a clone for the current issue among the issues in the destination project. But the error we are getting creates two identical clones (except for the issue key) within the destination project. We are preventing it by checking for an identical summary (shown as bold in code), though the business logic does not approve this workaround because the user should be able to have identical summaries in both projects. And the correct solution should be to check for issues with an existing link with the source issue but this is not working.

We are also using another listener to copy all links (which are not master-clone) from the master to the clones. And maybe the problem lays here, when the clone is created and the link to its master is added (this activates the second listener). I'll leave the code for this one as a comment.

Sorry for the huge amount of log warnings.

//Imports
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys
import com.onresolve.scriptrunner.canned.jira.utils.AbstractCloneIssue
import com.onresolve.scriptrunner.canned.jira.utils.CannedScriptUtils
import com.onresolve.scriptrunner.canned.jira.utils.ConditionUtils
import com.onresolve.scriptrunner.canned.jira.workflow.postfunctions.CloneIssue
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import groovy.xml.MarkupBuilder
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.web.bean.PagerFilter

//Variables and context
def issue = event.issue
def project = issue.getProjectObject().name
def targetProject = ComponentAccessor.customFieldManager.getCustomFieldObject("customfield_16400").getValue(issue)
def issueToCloneKey = issue.getKey()
def issueToClone = ComponentAccessor.issueManager.getIssueByCurrentKey(issueToCloneKey)
def linkBetweenIssues = CannedScriptUtils.getAllLinkTypesWithInwards(true).find { it.value == "clones" }?.key?.toString()
def inwardCondition = ComponentAccessor.issueLinkManager.getInwardLinks(event.issue.id)*.issueLinkType.name.contains('Cloners')
def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchService = ComponentAccessor.getComponent(SearchService)
boolean exists = false

//Custom fields
def tranferAssessmentCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16500)
def transferredAssessmentCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16604)
def tranferSeverityCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16501)
def transferredSeverityCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16605)
def tranferProbabilityCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16502)
def transferredProbabilityCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16606)
def assessCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16201)
def severityCf = ComponentAccessor.customFieldManager.getCustomFieldObject(15537)
def pSeverityCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16304)
def pProbabilityCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16303)
def probabilityCf = ComponentAccessor.customFieldManager.getCustomFieldObject(15507)
def qualitativeRatCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16202)
def financialImpactCf = ComponentAccessor.customFieldManager.getCustomFieldObject(15505)
def topicCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16200)
def waivedCf = ComponentAccessor.customFieldManager.getCustomFieldObject(15510)
def othersCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16203)
def discRequiredCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16204)
def responseStrategyCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16205)
def destinationProjectCf = ComponentAccessor.customFieldManager.getCustomFieldObject(16400)


//The value for the custom field Destination Project should not be empty
if(targetProject != null){
    //Using a JQL Query to bring all the issues from the target project
    //Warning: Please, be cautious with the method getUnlimitedFilter(), if you have a lot of issues in a project there can be complications.
    def jqlSearch = "project = '" + targetProject.getKey() + "' AND issuetype = 'Risk'"
    log.warn("JQL SEARCH: " + jqlSearch)
    def query = jqlQueryParser.parseQuery(jqlSearch)
    log.warn("QUERY: "+ query)
    def results = searchService.search(user, query, PagerFilter.getUnlimitedFilter())
    log.warn("THE QUERY RETURNED: " + results.getResults())
    if (results.getTotal() == 0) {
        log.warn("TARGET PROJECT IS EMPTY")
        return
    }

    //This code iterates through all of the issues within the Destination Project
    results.getResults().each{document ->
        def issueKey = document.key
        def riskIssue = ComponentAccessor.issueManager.getIssueObject(issueKey)
        def summary = riskIssue.getSummary()
        log.warn("ENTERS TO CHECK ALL RISKS")
        //JQL to find clones belonging (linked) to current master risk (source issue) in the selected destination project
        def currentIssueKey = issue.getKey()
        def jqlLinkType = "project = '" + targetProject.getName() + "' AND issuefunction in hasLinkType('Cloners') AND issue in linkedIssues(" + currentIssueKey + ")"
        def query2 = jqlQueryParser.parseQuery(jqlLinkType)
        def results2 = searchService.search(user, query2, PagerFilter.getUnlimitedFilter())

        //Check if clone already exists in destination project and prevents re-creation
        if(results2.getTotal() != 0 || summary == issue.getSummary()){
            exists = true
            log.warn("A CLONE ALREADY EXISTS IN THIS DESTINATION PROJECT")
            return
        }
       
    }
}
else{
    return
}

//If Destination Project is not the same as source project and the clone doesn't exists already.
if(targetProject != null && targetProject.key != issue.getProjectObject().getKey() && exists == false){

    def targetProjectKey = targetProject.getKey()

    log.warn("TARGET PROJECT KEY: " + targetProjectKey)
    log.warn("IS IT CLONE?: " + inwardCondition)
    log.warn("ISSUE TYPE: " + issue.getIssueType().name)
    // Set the creation parameters/inputs (use clone issue with link type)
    def params = [
        (CloneIssue.FIELD_TARGET_PROJECT)       : targetProjectKey,
        (CloneIssue.FIELD_TARGET_ISSUE_TYPE)    : null,
        (CloneIssue.FIELD_COPY_FIELDS)          : AbstractCloneIssue.COPY_ALL_FIELDS,
        (CloneIssue.FIELD_SELECTED_FIELDS)      : null,
        (CloneIssue.FIELD_COPY_COMMENTS)        : false,
        (CloneIssue.FIELD_USER_KEY)             : null,
        (ConditionUtils.FIELD_ADDITIONAL_SCRIPT): ["", ""],
        (CloneIssue.FIELD_LINK_TYPE)            : linkBetweenIssues
    ] as Map<String, Object>
    def executionContext = [issue: issueToClone] as Map<String, Object>

    if(issue.getIssueType().getName() == "Risk" && !inwardCondition){
        log.warn("STARTS CLONING")
        def cloneIssueAction = ScriptRunnerImpl.scriptRunner.createBean(CloneIssue)
        // Execute the clone action with the specified params
        def updatedExecutionContext = cloneIssueAction.execute(params, executionContext)

        // Return the link to the cloned issue
        cloneIssueAction ? "Cloned issue: ${createHrefLinkToIssue(updatedExecutionContext.newIssue as String)}" :
            "Could not clone issue, check the logs for errors"

        def newIssue = ComponentAccessor.getIssueManager().getIssueObject(updatedExecutionContext.newIssue as String)
        def oLinks = ComponentAccessor.issueLinkManager.getOutwardLinks(newIssue.id)

        for(o in oLinks){
            if(o.getLinkTypeId() == 10001){
                ComponentAccessor.issueLinkManager.removeIssueLink(o, user)
            }
        }
       
        //Deletes the value for the fields which are not needed in the Destination Project
        newIssue.setCustomFieldValue(qualitativeRatCf, null)
        newIssue.setCustomFieldValue(financialImpactCf, null)
        newIssue.setCustomFieldValue(topicCf, null)
        newIssue.setCustomFieldValue(waivedCf, null)
        newIssue.setCustomFieldValue(othersCf, null)
        newIssue.setCustomFieldValue(discRequiredCf, null)
        newIssue.setCustomFieldValue(responseStrategyCf, null)
        newIssue.setCustomFieldValue(destinationProjectCf, null)
       

        //Check if the optional values will be transferred and copies them from the Master Risk (source issue)
        if(issue.getCustomFieldValue(tranferAssessmentCf) != null){
            newIssue.setCustomFieldValue(transferredAssessmentCf,tranferAssessmentCf.getValue(issue).toString())
        }
        else{
            newIssue.setCustomFieldValue(assessCf, null)
        }
       
        if(issue.getCustomFieldValue(tranferSeverityCf) != null){
            newIssue.setCustomFieldValue(transferredSeverityCf,tranferSeverityCf.getValue(issue).toString())
        }
        else{
            newIssue.setCustomFieldValue(pSeverityCf, null)
        }
       
        if(issue.getCustomFieldValue(tranferProbabilityCf) != null){
            newIssue.setCustomFieldValue(transferredProbabilityCf,tranferProbabilityCf.getValue(issue).toString())
        }
        else{
            newIssue.setCustomFieldValue(pProbabilityCf, null)
        }
        ComponentAccessor.getIssueManager().updateIssue(user,newIssue,EventDispatchOption.ISSUE_UPDATED, false)
    }
}

//Method to create link to master risk (source issue)
static String createHrefLinkToIssue(String issueKey) {
    def baseUrl = ComponentAccessor.applicationProperties.getString(APKeys.JIRA_BASEURL)
    def writer = new StringWriter()
    def html = new MarkupBuilder(writer)

    html.html {
        a href: "$baseUrl/browse/$issueKey", issueKey
    }

    writer
}

1 answer

0 votes
Juan Felipe Garcia Carreño January 24, 2023
import com.atlassian.jira.component.ComponentAccessor
//Variables
def issue = event.getIssueLink().getSourceObject()
def destIssue = event.getIssueLink().getDestinationObject()
def oLinks = ComponentAccessor.issueLinkManager.getOutwardLinks(issue.id)
def clone = null
def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
boolean inwardCondition = ComponentAccessor.issueLinkManager.getInwardLinks(issue.id)*.issueLinkType.name.contains('Cloners')


log.warn("LINK TO BE CREATED BETWEEN: "+ issue.getKey() + " AND " + destIssue.getKey())
log.warn("LINK: "+ event.getIssueLink().getId())
log.warn("LINK TYPE: "+ event.getIssueLink().getIssueLinkType().getId())



//Checks if the link is outwards (issue's Issue Type is "Risk") or inwards (issue's Issue Type is not "Risk")
if(issue.issueType.name != "Risk" && destIssue.issueType.name == "Risk"){
    oLinks = ComponentAccessor.issueLinkManager.getOutwardLinks(destIssue.id)
    inwardCondition = ComponentAccessor.issueLinkManager.getInwardLinks(destIssue.id)*.issueLinkType.name.contains('Cloners')
    //Checks if current issue is not a clone
    if(!inwardCondition){
        log.warn("IS MASTER RISK")
        //Iterates over the issue links
        for(o in oLinks){
            //Finds the related transparent clones
            if(o.getLinkTypeId() == 10001){
                log.warn("THE CLONES ARE: "+ oLinks)
                clone = o.getDestinationObject()
                //Checks if the new link is not a Cloners link type.
                if(issue.id != clone.id && event.getIssueLink().getLinkTypeId() != 10001){
                    //Adds the new link to all the related transparent clones.
                    log.warn("CREATES A NEW LINK BETWEEN "+ clone.getKey() + " AND " + issue.getKey())
                    ComponentAccessor.getIssueLinkManager().createIssueLink(issue.id,clone.id,event.getIssueLink().getLinkTypeId(),1,user)
                }
            }
        }
    }
}
else if (issue.issueType.name == "Risk" && destIssue.issueType.name != "Risk"){
    //Checks if current issue is not a clone
    if(!inwardCondition){
        log.warn("IS MASTER RISK")
        //Iterates over the issue links
        for(o in oLinks){
            //Finds the related transparent clones
            if(o.getLinkTypeId() == 10001){
                log.warn("THE CLONES ARE: "+ oLinks)
                clone = o.getDestinationObject()
                //Checks if the new link is not a Cloners link type.
                if(destIssue.id != clone.id && event.getIssueLink().getLinkTypeId() != 10001){
                    //Adds the new link to all the related transparent clones.
                    log.warn("CREATES A NEW LINK BETWEEN "+ clone.getKey() + " AND " + destIssue.getKey())
                    ComponentAccessor.getIssueLinkManager().createIssueLink(clone.id,destIssue.id,event.getIssueLink().getLinkTypeId(),1,user)
                }
            }
        }
    }
}
else if(issue.getIssueType().getName() == destIssue.getIssueType().getName()){
    log.warn("SOURCE AND DESTINATION ISSUES ARE RISKS")
    def inwardCondition2 = ComponentAccessor.issueLinkManager.getInwardLinks(destIssue.id)*.issueLinkType.name.contains('Cloners')
    if(inwardCondition && inwardCondition2){
        log.warn("SOURCE AND DESTINATION ISSUES ARE CLONES")
        oLinks = ComponentAccessor.issueLinkManager.getOutwardLinks(issue.id)
        for (o in oLinks){
            if(o.getLinkTypeId() == 10001){
                ComponentAccessor.getIssueLinkManager().removeIssueLink(o,user)
            }
        }
        oLinks = ComponentAccessor.issueLinkManager.getOutwardLinks(destIssue.id)
        for (o in oLinks){
            if(o.getLinkTypeId() == 10001){
                ComponentAccessor.getIssueLinkManager().removeIssueLink(o,user)
            }
        }
    }
}

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
CLOUD
PRODUCT PLAN
PREMIUM
TAGS
AUG Leaders

Atlassian Community Events