Synced sub-tasks become standard_issue_type on remote

Originally asked by Johannes Glück on 11 February 2022 (original question)


Hi

Exalate works great for us…

However we now try to add subtask functionality on both sides… And the snippets taken from your knowledgebase lead to tickets with the proper ticket type, but are not added as subticket to the task on the receiving side.

Can you please help?

INGOING SCRIPT

/*

CUSTOM VERSION 1.9 FOR XXX RECEPTION DESK - Copyright XXX AG
for XXX.com
*/
def PROJECT_KEY = "TRD"
if(firstSync){
   issue.projectKey   = PROJECT_KEY
   // Set type name from source issue, if not found set a default
   issue.typeName     = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"
}
if (replica.project.key != PROJECT_KEY) {
  issue.summary = "Duplikat - Duplicate"
  issue.comments = commentHelper.addComment("Ticket wurde auf XXX Seite als Klon in ein internes Projekt zur weiteren Bearbeitung verschoben und bleibt erhalten, für den Fall, dass es sich hierbei um einen Fehler handelt.", issue.comments)
  issue.resolution = nodeHelper.getResolution("Duplicate")
  issue.setStatus("Won't Fix")
  // stop sync if the local issue has been moved from the project - see https://docs.idalko.com/exalate/display/ED/How+to+unexalate+moved+issues+automatically#Howtounexalatemovedissuesautomatically-Stopthesynciftheissuehasbeenmovedtoanotherproject
  // does not work right now (the implementation in the page either)
  // AND perhaps (if moving back) it is better not to unexalate
//  final def _syncInitService = com.atlassian.jira.component.ComponentAccessor
//    .getOSGiComponentInstanceOfType(com.exalate.api.trigger.ISyncInitiationService.class)
//  _syncInitService.unexalate(issue.key)
  return
}
issue.typeName     = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"
issue.summary      = replica.summary
issue.description  = replica.description
issue.attachments  = attachmentHelper.mergeAttachments(issue, replica)
issue.labels       = replica.labels
issue.priority     = replica.priority
issue.environment  = replica.environment
issue.issueLinks   = replica.issueLinks
issue.created      = replica.created
issue.updated      = replica.updated

/*
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.getUserByUsername("XXX")
issue.reporter = nodeHelper.getUserByUsername(replica.reporter?.username) ?: defaultUser
issue.assignee = nodeHelper.getUserByUsername(replica.assignee?.username) ?: 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.getUserByUsername(it.author?.username) }

/*
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
Custom Fields
- Reproduce Steps
- Actual result
- Expected Result

issue.customFields."CF Name".value = replica.customFields."CF Name".value
*/
if ( replica.customFields.containsKey("Reproduce Steps") ) issue.customFields."Reproduce Steps".value    = replica.customFields."Reproduce Steps".value
if ( replica.customFields.containsKey("Actual result") )   issue.customFields."Actual result".value      = replica.customFields."Actual result".value
if ( replica.customFields.containsKey("Expected Result") ) issue.customFields."Expected Result".value    = replica.customFields."Expected Result".value
if ( replica.customFields.containsKey("Detected on stage") ) issue.customFields."10400".value            = replica.customFields."Detected on stage".value

/* Deal with new components */
issue.components = replica.components.collect { component ->
def remoteComponentLeadUsername = component.lead?.username
def localComponentLead = nodeHelper.getUserByUsername(remoteComponentLeadUsername)
nodeHelper.createComponent(
    issue,
    component.name,
    component.description, // can also be null
    localComponentLead?.key, // can also be null
    component.assigneeType?.name() ?: "UNASSIGNED" // set a default as unassigned if there's no assignee type for the component
    )
}

/* Deal with components in an issue */
issue.components = replica.components
   .collect { remoteComponent ->
        nodeHelper.getComponent(
            remoteComponent.name,
            nodeHelper.getProject(issue.projectKey)
        )
    }.findAll()

/* Deal with Epics */
if(replica.issueType.name == "Epic") {
    issue.customFields."Epic Name".value = replica.customFields."Epic Name"?.value
}
Epic.receive()

/* Deal with subtasks */
if(firstSync && replica.parentId){
    issue.typeName     = "Sub-Task" //Make sure to use the right subtask type here.
    def localParent = nodeHelper.getLocalIssueFromRemoteId(replica.parentId.toLong())
    if(localParent){
        issue.parentId = localParent.id
    } else {
       throw new com.exalate.api.exception.IssueTrackerException("Subtask cannot be created: parent issue with remote id " + replica.parentId + " was not found. Please make sure the parent issue is synchronized before resolving this error" )
    }
}

OUTGOING SCRIPT

/*

CUSTOM VERSION 1.9 FOR XXX RECEPTION DESK - Copyright XXX AG
for XXX.com
*/
replica.key            = issue.key
replica.type           = issue.type
replica.assignee       = issue.assignee
replica.reporter       = issue.reporter
replica.summary        = issue.summary
replica.description    = issue.description
replica.labels         = issue.labels
// do not export "internal" comments, see https://docs.idalko.com/exalate/display/ED/filterLocal
replica.comments       = issue.comments.findAll { !it.internal }
replica.status         = issue.status
replica.parentId       = issue.parentId
replica.priority       = issue.priority
replica.attachments    = issue.attachments
replica.project        = issue.project
replica.issueLinks     = issue.issueLinks
replica.components     = issue.components
replica.environment    = issue.environment
replica.created        = issue.created
replica.updated        = issue.updated

//Comment these lines out if you are interested in sending the full list of versions and components of the source project.
//replica.project.versions = []
//replica.project.components = []

/*
Custom Fields
- Reproduce Steps
- Actual result
- Expected Result
*/
if ( issue.customFields.containsKey("11705") ) replica.customFields."Reproduce Steps"  = issue.customFields."11705"
if ( issue.customFields.containsKey("11707") ) replica.customFields."Actual result"    = issue.customFields."11707"
if ( issue.customFields.containsKey("11706") ) replica.customFields."Expected Result"  = issue.customFields."11706"
if ( issue.customFields.containsKey("13300") ) replica.customFields."Detected on stage"= issue.customFields."13300"

/* Deal with Epics */
if(issue.issueType.name == "Epic") {
    // ID is needed, as - when Epic Name is empty - only the ID would be unique (reason unknown)
    // For fields in internal jira, please look here: https://XXX/rest/api/latest/field
    replica.customFields."Epic Name" = issue.customFields."10011"
}
Epic.sendInAnyOrder()


Answer by Johannes Glück on 30 June 2022

Hi all, hi Francis Martens (Exalate) ,

found the bug. The parentId is not necessarily sent during the first sync (both ways by our Jira instances), so the

if(firstSync && replica.parentId){

should be like that:

if(firstSync && replica.parentId){

This should also be done, when you want to stay open of tickets that are changed from standardtasks to subtasks or vice versa.

Cheers, Johannes


Answer by Francis Martens (Exalate) on 11 February 2022

Can you focus for a moment on following code piece

/* Deal with subtasks */
if(firstSync && replica.parentId){
    issue.typeName     = "Sub-Task" //Make sure to use the right subtask type here.
    def localParent = nodeHelper.getLocalIssueFromRemoteId(replica.parentId.toLong())
    if(localParent){
        issue.parentId = localParent.id
    } else {
       throw new com.exalate.api.exception.IssueTrackerException("Subtask cannot be created: parent issue with remote id " + replica.parentId + " was not found. Please make sure the parent issue is synchronized before resolving this error" )
    }
}

You should check if

  • replica.parentId is set
  • The block is being entered into
  • The subtask type name is correct (sometimes it is ‘sub task’) or a variation
  • Is the localParent found (is it set)

When wondering how, use debug.error

debug.error("Show me ${someValue}")

Comments:

Johannes Glück commented on 06 April 2022

Hi thanks. checked all.

it showed, that sometimes in firstSync the parentId is null… So after removing && firstSync it works.

firstSync would also avoid handling a subtask, which was moved from a standardissuetype to become one.

Are there any performanceissues? If not, I would recommend to alter the sample.

Cheers, Johannes