Sprint synchronization - Multiple entities touched on first sync

Originally asked by Harold Oconitrillo on 09 November 2022 (original question)


I have some weird issues when trying to synchronize sprints between Jira Cloud instances. I followed up to your documentation and I receive error “Multiple entities touched on first sync”.
Likewise, I have tried to remove a conditional if(first sync), move it to a different place, but with no luck.
Whole configuration based on :https://docs.idalko.com/exalate/display/ED/How+to+sync+sprints+in+Jira+Cloud

/*
if(firstSync){
   issue.projectKey   = "SYNC"
   // Set type name from source issue, if not found set a default
   issue.typeName     = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"
}
*/

//entityType represent the type of the remote side entity
if(entityType == "sprint"){
    //Executed when receiving a sprint sync from the remote side
    def sprintMap = ["160":"25"] //[remoteBoardId: localBoardId]
     
    sprint.name = replica.name
    sprint.goal = replica.goal
    sprint.state = replica.state
    sprint.startDate = replica.startDate
    sprint.endDate = replica.endDate
    def localBoardId = sprintMap[replica.originBoardId]
    if(localBoardId == null){
       throw new com.exalate.api.exception.IssueTrackerException("No board mapping for remote board id "+replica.originBoardId)
    }
    sprint.originBoardId = localBoardId //Set the board ID where the sprint will be created
}
if(entityType == "issue"){
    //Executed when receiving an issue sync from the remote side
    if(firstSync){
        issue.projectKey = "BMS"
        issue.typeName = "Task"
    }
    issue.summary = replica.summary
    issue.description = replica.description
    issue.comments     = commentHelper.mergeComments(issue, replica)
    issue.attachments  = attachmentHelper.mergeAttachments(issue, replica)
    issue.labels       = replica.labels
    //....
    //other script rules to sync issues
    //....
    def remoteSprintId = replica.customFields.Sprint?.value?.find { it.state.toUpperCase() != "CLOSED" }?.id
    if(remoteSprintId){
       def localSprintId = nodeHelper.getLocalIssueKeyFromRemoteId(remoteSprintId, "sprint")?.id
         if(localSprintId){
            issue.customFields.Sprint.value = localSprintId
        }
    }
}



if (replica.parentId) {
   // look up the twin parent
   def localParent = nodeHelper.getLocalIssueFromRemoteId(replica.parentId.toLong())
   // if found, then set it, else don't create the subtask
  if(localParent) {
     issue.parentId = localParent.id
    }
}
 

def defaultUser = nodeHelper.getUserByEmail("client@strix.app")
//issue.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
issue.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
 

/*
User Synchronization (Assignee/Reporter)

Set a Reporter/Assignee from the source side, if the user can't be found set a default user
You can use this approach for custom fields of type User
def defaultUser = nodeHelper.getUserByEmail("default@idalko.com")
issue.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
issue.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
*/

/*
Comment Synchronization

Sync comments with the original author if the user exists in the local instance
Remove original Comments sync line if you are using this approach
issue.comments = commentHelper.mergeComments(issue, replica){ it.executor = nodeHelper.getUserByEmail(it.author?.email) }
*/

/*
Status Synchronization

Sync status according to the mapping [remote issue status: local issue status]
If statuses are the same on both sides don't include them in the mapping
def statusMapping = ["Open":"New", "To Do":"Backlog"]
def remoteStatusName = replica.status.name
issue.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)
*/

/*
Custom Fields

This line will sync Text, Option(s), Number, Date, Organization, and Labels CFs
For other types of CF check documentation
issue.customFields."CF Name".value = replica.customFields."CF Name".value
*/

Comments:

Harold Oconitrillo commented on 10 November 2022

Hi,

Basically, all the rules related to issue entity should be in the ifblock for issue

/*
if(firstSync){
  issue.projectKey  = "SYNC"
  // Set type name from source issue, if not found set a default
  issue.typeName   = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"
}
*/
//entityType represent the type of the remote side entity
if(entityType == "sprint"){
    //Executed when receiving a sprint sync from the remote side
    def sprintMap = ["160":"25"] //[remoteBoardId: localBoardId]

    sprint.name = replica.name
    sprint.goal = replica.goal
    sprint.state = replica.state
    sprint.startDate = replica.startDate
    sprint.endDate = replica.endDate
    def localBoardId = sprintMap[replica.originBoardId]
    if(localBoardId == null){
        throw new com.exalate.api.exception.IssueTrackerException("No board mapping for remote board id "+replica.originBoardId)
    }
    sprint.originBoardId = localBoardId //Set the board ID where the sprint will be created
}
if(entityType == "issue"){
    //Executed when receiving an issue sync from the remote side
    if(firstSync){
        issue.projectKey = "BMS"
        issue.typeName = "Task"
    }
    issue.summary = replica.summary
    issue.description = replica.description
    issue.comments   = commentHelper.mergeComments(issue, replica)
    issue.attachments = attachmentHelper.mergeAttachments(issue, replica)
    issue.labels    = replica.labels
    //....
    //other script rules to sync issues
    //....
    def remoteSprintId = replica.customFields.Sprint?.value?.find { it.state.toUpperCase() != "CLOSED" }?.id
    if(remoteSprintId){
        def localSprintId = nodeHelper.getLocalIssueKeyFromRemoteId(remoteSprintId, "sprint")?.id
        if(localSprintId){
            issue.customFields.Sprint.value = localSprintId
        }
    }
    if (replica.parentId) {
        // look up the twin parent
        def localParent = nodeHelper.getLocalIssueFromRemoteId(replica.parentId.toLong())
        // if found, then set it, else don't create the subtask
        if(localParent) {
            issue.parentId = localParent.id
        }
    }

    def defaultUser = nodeHelper.getUserByEmail("client@strix.app")
    //issue.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
    issue.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser

    /*
    User Synchronization (Assignee/Reporter)
    Set a Reporter/Assignee from the source side, if the user can't be found set a default user
    You can use this approach for custom fields of type User
    def defaultUser = nodeHelper.getUserByEmail("default@idalko.com")
    issue.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
    issue.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
    */
    /*
    Comment Synchronization
    Sync comments with the original author if the user exists in the local instance
    Remove original Comments sync line if you are using this approach
    issue.comments = commentHelper.mergeComments(issue, replica){ it.executor = nodeHelper.getUserByEmail(it.author?.email) }
    */
    /*
    Status Synchronization
    Sync status according to the mapping [remote issue status: local issue status]
    If statuses are the same on both sides don't include them in the mapping
    def statusMapping = ["Open":"New", "To Do":"Backlog"]
    def remoteStatusName = replica.status.name
    issue.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)
    */
    /*
    Custom Fields
    This line will sync Text, Option(s), Number, Date, Organization, and Labels CFs
    For other types of CF check documentation
    issue.customFields."CF Name".value = replica.customFields."CF Name".value
    */
}

Feel free to contact us back for further questions you may have.

Kind regards.