Images getting lost in Sync

Originally asked by Bhakti Prasad Panda on 18 May 2022 (original question)


When syncing description or any wiki field from Azure to Jira on-Prem, images are not getting copied properly. Its shows thumbnail of the image instead of actual image like below:

Is there any reason for same? Is there any way to fix the same.

Also when syncing from Jira to Azure it gives below line:

I am using the transform scripts for the syncing and both scripts are having some loopholes it seems.

Can anyone help?


Answer by Mathieu Lepoutre on 13 July 2023

Hi

Update as of 13 July.

Outgoing sync Jira On Prem:

import com.atlassian.jira.component.ComponentAccessor

class WikiToHtml {
	static String transform(String wikiFormat) {
		if (!wikiFormat) {
			return null
		}

		// access the correct services
		def jcl = ComponentAccessor.classLoader
		def app = ComponentAccessor.getApplicationProperties()
		def epubClass = jcl.loadClass("com.atlassian.event.api.EventPublisher")
		def epub = ComponentAccessor.getOSGiComponentInstanceOfType(epubClass)
		def fmanClass = jcl.loadClass("com.atlassian.jira.config.FeatureManager")
		def fman = ComponentAccessor.getOSGiComponentInstanceOfType(fmanClass)
		def vreqClass = jcl.loadClass("com.atlassian.jira.util.velocity.VelocityRequestContextFactory")
		def vreq = ComponentAccessor.getOSGiComponentInstanceOfType(vreqClass)
		def wrenderClass = jcl.loadClass("com.atlassian.jira.issue.fields.renderer.wiki.AtlassianWikiRenderer")
		def wrender = wrenderClass.newInstance(epub, app, vreq, fman)


		def fixImage = wikiFormat?.replaceAll(/\!(\S+)\|\S+\!/, '<!-- inline image filename=#$1# -->')
		fixImage = fixImage.replaceAll(/\!\^(\S+)\|\S+\!/, '<!-- inline image filename=#$1# -->')
		fixImage = fixImage.replaceAll(/\!\^(\S+)\!/, '<!-- inline image filename=#$1# -->')
		fixImage = fixImage.replaceAll(/\!(\S+)\!/, '<!-- inline image filename=#$1# -->')

		// wiki text can also contain files
		fixImage = fixImage.replaceAll(/\[(\S+)\|\^(\S+)\]/, '<!-- inline file filename=#$2# -->')
		fixImage = fixImage.replaceAll(/\[\^(\S+)\]/, '<!-- inline file filename=#$1# -->')
		return wrender.render(fixImage, null)

	}

}

replica.description = WikiToHtml.transform(issue.description)
replica.labels         = issue.labels
replica.comments       = issue.comments.collect {
    comment -> 
    comment.body = WikiToHtml.transform (comment.body)
    comment
}

Incoming Sync Azure Devops:

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

workItem.summary  = replica.summary
  
workItem.attachments  = attachmentHelper.mergeAttachments(workItem, replica)
 
 
def processInlineImages = { str ->
    def processUnescapedLtGtTags = {
        def counter = 0
        while (counter < 1000) {
            def matcher = (str =~ /<!-- inline image filename=#(([^#]+)|(([^#]+)#([^#]+)))# -->/)
            if (matcher.size() < 1) {
                break;
            }
            def match = matcher[0]
            if (match.size() < 2) {
                break;
            }
            //log.error("replica.attachments=${replica.attachments}")
            def attId = replica.attachments.find { it.filename?.equals(match[1]) }?.remoteId
            if (!attId) {
                log.error("""Could not find attachment with name ${match[1]}, 
           known names: ${replica.attachments.filename}, 
           match: ${replica.attachments.find { it.filename?.equals(match[1]) }}
       """)
                str = str.replace(match[0], """<!-- inline processed image filename=#${match[1]}# -->""".toString())
            } else {
                def tmpStr = str.replace(match[0], """<img src="/secure/attachment/${attId}/${attId}_${match[1]}" />""".toString())
                if (tmpStr == str) {
                    break;
                }
                str = tmpStr
            }
            counter++
        }
        str
    }
    def processLtGtTags = {
        def counter = 0
        while (counter < 1000) {
            def matcher = (str =~ /<!-- inline image filename=#(([^#]+)|(([^#]+)#([^#]+)))# -->/)
            if (matcher.size() < 1) {
                break;
            }
            def match = matcher[0]
            if (match.size() < 2) {
                break;
            }
            def attId = replica.attachments.find { it.filename?.equals(match[1]) }?.remoteId
            if (!attId) {
                log.error("""Could not find attachment with name ${match[1]}, 
           known names: ${replica.attachments.filename}, 
           match: ${replica.attachments.find { it.filename?.equals(match[1]) }}
       """)
                str = str.replace(match[0], """<!-- inline processed image filename=#${match[1]}# -->""".toString())
            } else {
                def tmpStr = str.replace(match[0], """<img src="/secure/attachment/${attId}/${attId}_${match[1]}" />""".toString())
                if (tmpStr == str) {
                    break;
                }
                str = tmpStr
            }
            counter++
        }
        str
    }
    def processNoImage = {
        //"<p><img
        // src=\"https://jira.smartodds.co.uk/images/icons/attach/noimage.png\"
        // imagetext=\"Screenshot from 2022-11-18 11-09-25.png|thumbnail\"
        // align=\"absmiddle\"
        // border=\"0\" /></p>"
        def counter = 0
        while (counter < 1000) {
            def matcher = (str =~ /<img src="[^"]+" imagetext="(([^"]+)\|thumbnail)" align="absmiddle" border="0" \/>/)
            if (matcher.size() < 1) {
                break;
            }
            def match = matcher[0]
            if (match.size() < 2) {
                break;
            }
            def filename = match[2]
            def attId = replica.attachments.find { it.filename?.equals(filename) }?.remoteId
            if (!attId) {
                log.error("""Could not find attachment with name ${filename}, 
           known names: ${replica.attachments.filename}, 
           match: ${replica.attachments.find { it.filename?.equals(filename) }}
       """)
                str = str.replace(match[0], """<img src="/images/icons/attach/noimage.png" processed imagetext="$filename|thumbnail" align="absmiddle" border="0" />""".toString())
            } else {
                def tmpStr = str.replace(match[0], """<img src="/secure/attachment/${attId}/${attId}_${filename}" />""".toString())
                if (tmpStr == str) {
                    break;
                }
                str = tmpStr
            }
            counter++
        }
        str
    }
    def processImgTagsWithIds = {
        //"<p>TEST DECS23456 </p> \n
        //<p><span class=\"image-wrap\" style=\"\"><img src=\"/rest/api/3/attachment/content/36820\"></span></p> \n
        //<p>TESt </p> \n
        //<p><span class=\"image-wrap\" style=\"\"><img src=\"/rest/api/3/attachment/content/36821\"></span></p> \n
        //<p>and more</p>"
        def counter = 0
        while (counter < 1000) {
            def matcher = (str =~ /<img src="\/rest\/api\/3\/attachment\/content\/(\d+)">/)
            if (matcher.size() < 1) {
                return str
            }
            def match = matcher[0]
            //println("match[1]=$match[1]")
            if (match.size() < 2) { // match[0]=<img src="/rest/api/3/attachment/content/36820"> match[1]=36820
                return str
            }
            def attId = match[1]
            def attachment = replica.attachments.find { (it.remoteId as String) == ( attId as String ) }
            if (!attachment) {
                log.error("""Could not find attachment with id ${attId}, 
           known ids: ${replica.attachments.remoteId}, 
           match: ${replica.attachments.find { (it.remoteId as String) == ( attId as String ) }}
       """)
                str = str.replace(match[0], """<img src="/rest/api/3/attachment/content/${attId}" processed />""".toString())
            } else {
                def tmpStr = str.replace(match[0], """<img src="/secure/attachment/${attId}/${attId}_${attachment.filename}" />""".toString())
                if (tmpStr == str) {
                    break;
                }
                str = tmpStr
            }
            counter++
        }
        str
    }
    //log.error("#processimages 0 $str")
    str = processUnescapedLtGtTags()
    //log.error("#processimages 1 $str")
    str = processLtGtTags()
    //log.error("#processimages 2 $str")
    str = processNoImage()
    //log.error("#processimages 3 $str")
    str = processImgTagsWithIds()
    log.error("#processimages $str")
    str
}
            
workItem.comments     = commentHelper.mergeComments(workItem, replica, {
    comment ->
def attrAuthor = comment.author?.displayName ?: "Default-"
    comment.body =  "<b> ${attrAuthor} said:</b> " + comment.body
    comment.body = processInlineImages (comment.body)
comment
})




workItem.description = processInlineImages(replica.description)

Thank you.

Kind regards,
Mathieu Lepoutre


Answer by Serhiy Onyshchenko on 01 December 2022

Hey, Bhakti Prasad Panda
Here’s a video showing, how image processing works in my environment:

Here’s the script for Jira on-prem’s Outgoing sync I’d used:

import com.exalate.transform.WikiToHtml
 
replica.key            = issue.key
replica.type           = issue.type
replica.assignee       = issue.assignee
replica.reporter       = issue.reporter
replica.summary        = issue.summary
replica.description    = WikiToHtml.transform(issue.description)
//replica.description    = issue.description
//replica.description    = nodeHelper.getHtmlField(issue, "description")
replica.labels         = issue.labels
replica.comments       = issue.comments
replica.resolution     = issue.resolution
replica.status         = issue.status
replica.parentId       = issue.parentId
replica.priority       = issue.priority
replica.attachments    = issue.attachments
replica.project        = issue.project
 
//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 = []
 
replica.customFields."Severity" = issue.customFields."Severity"
replica.customFields."Source" = issue.customFields."Source"
replica.customFields."14357" = issue.customFields."14357"
//replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria"
replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria"
replica.customFields."Story Points" = issue.customFields."Story Points"
 
/*
Custom Fields
 
replica.customFields."CF Name" = issue.customFields."CF Name"
*/

And here’s the incoming script for ADO I’d used:

//workItem.labels       = replica.labels
workItem.priority     = replica.priority
if(firstSync){
   // Set type name from source entity, if not found set a default
   if(replica.type?.name=="Story"){
       workItem.typeName = "User Story";
   }else {
       workItem.typeName = nodeHelper.getIssueType(replica.type?.name)?.name ?: "Issue";
   }
}
def defaultUser = nodeHelper.getUserByEmail("jirasync@comaround.onmicrosoft.com")
workItem.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser
workItem.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser
workItem.summary  = replica.summary
 
workItem.attachments  = attachmentHelper.mergeAttachments(workItem, replica)

store(issue)
def await = { f -> scala.concurrent.Await$.MODULE$.result(f, scala.concurrent.duration.Duration.apply(1, java.util.concurrent.TimeUnit.MINUTES)) }
def creds = await(httpClient.azureClient.getCredentials())
def issueTrackerUrl = creds.issueTrackerUrl()
def project = connection.trackerSettings.fieldValues."project"
def processInlineImages = { str ->
    def processLtGtTags = {
        def counter = 0
        while (counter < 1000) {
            def matcher = (str =~ /<!-- inline image filename=#(([^#]+)|(([^#]+)#([^#]+)))# -->/)
            if (matcher.size() < 1) {
                break;
            }
            def match = matcher[0]
            if (match.size() < 2) {
                break;
            }
            def filename = match[1]
            def attId = workItem.attachments.find { it.filename?.equals(filename) }?.idStr
            if (!attId) {
                log.error("""Could not find attachment with name ${filename},
           known names: ${replica.attachments.filename},
           match: ${replica.attachments.find { it.filename?.equals(filename) }}
       """)
                str = str.replace(match[0], """<!-- inline processed image filename=#${filename}# -->""".toString())
            } else {
                def tmpStr = str.replace(match[0], """<img src=\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=${filename}\"/>""".toString())
                if (tmpStr == str) {
                    break;
                }
                str = tmpStr
            }
            counter++
        }
        str
    }
    def processNoImage = {
        //"<p><img
        // src=\"https://jira..../images/icons/attach/noimage.png\"
        // imagetext=\"Screenshot from 2022-11-18 11-09-25.png|thumbnail\"
        // align=\"absmiddle\"
        // border=\"0\" /></p>"
        def counter = 0
        while (counter < 1000) {
            def matcher = (str =~ /<img src="[^"]+" imagetext="(([^"]+)\|thumbnail)" align="absmiddle" border="0" \/>/)
            if (matcher.size() < 1) {
                break;
            }
            def match = matcher[0]
            if (match.size() < 2) {
                break;
            }
            def filename = match[2]
            def attId = workItem.attachments.find { it.filename?.equals(filename) }?.idStr
            if (!attId) {
                log.error("""Could not find attachment with name ${filename},
           known names: ${replica.attachments.filename},
           match: ${replica.attachments.find { it.filename?.equals(filename) }}
       """)
                str = str.replace(match[0], """<img src="/images/icons/attach/noimage.png" processed imagetext="$filename|thumbnail" align="absmiddle" border="0" />""".toString())
            } else {
                /*
                def m = "#<a href=\"https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#\" class=\"external-link\" target=\"_blank\" rel=\"nofollow noopener\">https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#</a>" =~ /#<a([^>]+)>([^<]+)#<\/a>/
                m.iterator().next()[2]
                */
                //if (matchGroup1)
                //debug.error("matchGroup0=${matchGroup0} matchGroup1=$matchGroup1")
                def tmpStr = str.replaceAll(match[0], """<img src=\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=${filename}\"/>""".toString())
                if (tmpStr == str) {
                    break;
                }
                str = tmpStr
            }
            counter++
        }
        str
    }
    if (str == null) {
        return null
    }
    str = processLtGtTags()
    str = processNoImage()

    log.error("#processimages $str")
    str
}

String value = processInlineImages(replica.description)
 
if(replica.type?.name=="Bug"){
    workItem."Microsoft.VSTS.TCM.ReproSteps" = value
}else{
    workItem.description  = value
}
 
//debug.error("description = ${workItem."Microsoft.VSTS.TCM.ReproSteps"} value=${value}")
//workItem.description  = WikiToHtml.transform(replica.description)
workItem.labels       = replica.labels
//workItem.labels       = replica.labels.collect {it.label = it.label.replace("_", " "); it}
//workItem.attachments  = attachmentHelper.mergeAttachments(workItem, replica)
//workItem.iterationPath = replica.customFields."OTL code"?.value ?: default
replica.changedComments = []
//workItem.comments     = commentHelper.mergeComments(workItem, replica)
workItem.comments     = commentHelper.mergeComments(workItem, replica, { c ->
    c.body = processInlineImages(c.body)
    c
})
//workItem.comments = replica.comments
//debug.error(replica."Acceptance Criteria")
workItem."ComAround.Outcome" = processInlineImages(replica."Acceptance Criteria".value)
 
if(replica.customFields."Severity"?.value?.value!="1 - Severe"){
    if(replica.customFields."Severity"?.value?.value=="Medium"){
        workItem."Microsoft.VSTS.Common.Severity" = "3 - Medium"
    }else{
        workItem."Microsoft.VSTS.Common.Severity" = replica.customFields."Severity"?.value?.value  
    }
}else{
    workItem."Microsoft.VSTS.Common.Severity" = "1 - Critical"
}
 
//workItem."Microsoft.VSTS.Common.Severity" = replica.customFields."Severity"?.value?.value
/***/
if(replica.customFields."Source"?.value?.value=="Customer" || replica.customFields."Source"?.value?.value=="Security" || replica.customFields."Source"?.value?.value=="Internal"){
    workItem.customFields."Source".value = replica.customFields."Source"?.value?.value
}else{
    workItem.customFields."Source".value = "Internal"
}
workItem."comaround.ComAround.Customer" = replica.customFields."14357"?.value
//workItem."ComAround.Outcome" = WikiToHtml.transform(replica.customFields."Acceptance Criteria"?.value)
workItem."Microsoft.VSTS.Scheduling.StoryPoints" = replica.customFields."Story Points"?.value
//debug.error(replica.customFields."Source"?.value?.value)
 
def statusMapping = [
"Open":"New",
"Requirements Ready":"Ready",
"In Progress":"Development",
"Blocked":"Development",
"Code Review":"Development",
"Quality Review Failed":"Development",
"Doneness Failed":"Development",
"Reopened":"Development",
"Quality Review":"Testing",
"Doneness Review":"Verified",
"Closed":"Closed",
"Resolved":"Closed",
"Reopened":"New"
]
def remoteStatusName = replica.status?.name
if(replica.type?.name=="Bug" || replica.type?.name=="Story"){
    workItem.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)   
}
 
/*
Area Path Sync
This also works for iterationPath field
 
Set Area Path Manually
workItem.areaPath = "Name of the project\\name of the area"
 
Set Area Path based on remote side drop-down list
Change "area-path-select-list" to actual custom field name
workItem.areaPath = replica.customFields."area-path-select-list"?.value?.value
 
Set Area Path based on remote side text field
Change "area-path" to actual custom field name
workItem.areaPath = replica.customFields."area-path".value
*/
 
/*
Status Synchronization
 
Sync status according to the mapping [remote workItem status: local workItem status]
If statuses are the same on both sides don"t include them in the mapping
def statusMapping = ["Open":"New", "To Do":"Open"]
def remoteStatusName = replica.status.name
workItem.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName)
*/

Happy Exalating!

Regards, Serhiy.


Comments:

Serhiy Onyshchenko commented on 13 December 2022

Questions from Bhakti Prasad Panda

You have 2 methods in ADO incoming. One is “processInlineImages” and another is “processNoImage”. Will both methods be in picture? If yes then how?

only the processInlineImages will be called, and it will internally use the processLtGtTags and processNoImage , the function is to be used on any text field:

String value = processInlineImages(replica.description)
  
if(replica.type?.name=="Bug"){
    workItem."Microsoft.VSTS.TCM.ReproSteps" = value
}else{
    workItem.description  = value
}


and also

workItem.comments     = commentHelper.mergeComments(workItem, replica, { c ->
    c.body = processInlineImages(c.body)
    c
})

I needed help in syncing one wiki field also for which i created this ticket. I can’t see that field is mentioned in the incoming script of ADO. Please check and let me know.

Let me shoot another video with a field example.

Please expect an update by Thu Dec 15th 12:30 CET.
Regards, Serhiy.

Serhiy Onyshchenko commented on 14 December 2022

Hey, Bhakti Prasad Panda , here’s a video showing, how the same script is working for images in a custom field:

Outgoing in Jira:

//...replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria"
replica.customFields."Acceptance Criteria".value = WikiToHtml.transform(
issue.customFields."Acceptance Criteria".value
)

Incoming to ADO:

//...
def processInlineImages = { str ->
    def processLtGtTags = {
        def counter = 0
        while (counter < 1000) {
            def matcher = (str =~ //)
            if (matcher.size() < 1) {
                break;
            }
            def match = matcher[0]
            if (match.size() < 2) {
                break;
            }
            def filename = match[1]
            def attId = workItem.attachments.find { it.filename?.equals(filename) }?.idStr
            if (!attId) {
                log.error("""Could not find attachment with name ${filename},
           known names: ${replica.attachments.filename},
           match: ${replica.attachments.find { it.filename?.equals(filename) }}
       """)
                str = str.replace(match[0], """""".toString())
            } else {
                def tmpStr = str.replace(match[0], """![](\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=${filename}\"/)""".toString())
                if (tmpStr == str) {
                    break;
                }
                str = tmpStr
            }
            counter++
        }
        str
    }
    def processNoImage = {
        //"![](\"https://jira..../images/icons/attach/noimage.png\")

"
        def counter = 0
        while (counter < 1000) {
            def matcher = (str =~ /![]([^)/)
            if (matcher.size() < 1) {
                break;
            }
            def match = matcher[0]
            if (match.size() < 2) {
                break;
            }
            def filename = match[2]
            def attId = workItem.attachments.find { it.filename?.equals(filename) }?.idStr
            if (!attId) {
                log.error("""Could not find attachment with name ${filename},
           known names: ${replica.attachments.filename},
           match: ${replica.attachments.find { it.filename?.equals(filename) }}
       """)
                str = str.replace(match[0], """![](/images/icons/attach/noimage.png)""".toString())
            } else {
                /*
                def m = "#[https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#](\"https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#\")" =~ /#]+)>([^<]+)#<\/a>/
                m.iterator().next()[2]
                */
                //if (matchGroup1)
                //debug.error("matchGroup0=${matchGroup0} matchGroup1=$matchGroup1")
                def tmpStr = str.replaceAll(match[0], """![](\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=${filename}\"/)""".toString())
                if (tmpStr == str) {
                    break;
                }
                str = tmpStr
            }
            counter++
        }
        str
    }
    if (str == null) {
        return null
    }
    str = processLtGtTags()
    str = processNoImage()
 
    log.error("#processimages $str")
    str
}

workItem."Custom.HTML_Field" = processInlineImages(
    replica."Acceptance Criteria"
)

Regards, Serhiy.

Serhiy Onyshchenko commented on 09 January 2023

Hey, Bhakti Prasad Panda , I’d taken the liberty to take a look at a couple replicas synced in your tests, and the most recent I’d found was for https://jiraqa.bmc.com/browse/DRHD1-252 and it had the following text in the Acceptance Criteria:

From Jira
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat, sed consectetur magna porttitor.
*(Hard return)* Nullam a leo ultricies, lobortis turpis a, commodo mauris. Curabitur accumsan, nulla vel aliquam porttitor, neque dolor tempor mi, quis pretium nisi diam sed dui.
*(Soft return)* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat. 
 !^image.png|thumbnail!
 # Bullet point 1 
# Bullet point 2 
# Bullet point 3

So the

!^image.png|thumbnail!

was supposed to be replaced by this snippet in your outgoing:

replica.customFields."Acceptance Criteria".value = WikiToHtml.transform(
issue.customFields."Acceptance Criteria".value
)

Is it added to the end of the Jira’s outgoing sync script?
Or maybe I’m looking at a wrong example of an issue, could you guide me to a more recent one, please?
Regards, Serhiy.

Answer by Bhakti Prasad Panda on 13 July 2022

Hello Serhiy Onyshchenko ,

Please find the attached zip containing the configuration done on Azure and Jira end:

Exalate_Config.zip (old community)

We will wait for you response.

Regards
Bhakti


Comments:

Serhiy Onyshchenko commented on 13 July 2022

Thanks, Bhakti Prasad Panda , checking the config now to see, what could be going on with those images…

Serhiy Onyshchenko commented on 13 July 2022

Here’s the config from the zip in a side-by-side view (feel free to scroll):

ADO Jira
​Out ​ replica.key = workItem.key replica.assignee = workItem.assignee replica.summary = workItem.summary if(workItem.type.name=="Bug"){ replica.description = workItem."Microsoft.VSTS.TCM.ReproSteps" //debug.error(replica.description) }else{ replica.description = workItem.description } //replica.description = workItem.description replica.type = workItem.type replica.status = workItem.status //debug.error(replica.status) //debug.error(workItem.status) replica.labels = workItem.labels replica.priority = workItem.priority //replica.comments = nodeHelper.stripHtmlFromComments(workItem.comments) replica.comments = workItem.comments replica.attachments = workItem.attachments replica.project = workItem.project replica.areaPath = workItem.areaPath replica.reporter = workItem.reporter replica.iterationPath = workItem.iterationPath //replica.customFields."Severity" = workItem.customFields."Severity" replica.customKeys."Severity" = workItem."Microsoft.VSTS.Common.Severity" replica.customFields."Source" = workItem.customFields."Source" //replica.customKeys."Outcome" = nodeHelper.stripHtml(workItem."ComAround.Outcome") replica.customKeys."Outcome" = workItem."ComAround.Outcome" replica.customKeys."Customer" = workItem."comaround.ComAround.Customer" replica.customKeys."Story Points" = workItem."Microsoft.VSTS.Scheduling.StoryPoints" //Send a Custom Field value //replica.customFields."CF Name" = workItem.customFields."CF Name" ​ import com.exalate.transform.WikiToHtml replica.key = issue.key replica.type = issue.type replica.assignee = issue.assignee replica.reporter = issue.reporter replica.summary = issue.summary replica.description = WikiToHtml.transform(issue.description) //replica.description = issue.description //replica.description = nodeHelper.getHtmlField(issue, "description") replica.labels = issue.labels replica.comments = issue.comments replica.resolution = issue.resolution replica.status = issue.status replica.parentId = issue.parentId replica.priority = issue.priority replica.attachments = issue.attachments replica.project = issue.project //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 = [] replica.customFields."Severity" = issue.customFields."Severity" replica.customFields."Source" = issue.customFields."Source" replica.customFields."14357" = issue.customFields."14357" //replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria" replica.customFields."Acceptance Criteria" = issue.customFields."Acceptance Criteria" replica.customFields."Story Points" = issue.customFields."Story Points" /* Custom Fields replica.customFields."CF Name" = issue.customFields."CF Name" */
In ``` //workItem.labels = replica.labels workItem.priority = replica.priority if(firstSync){ // Set type name from source entity, if not found set a default if(replica.type?.name==“Story”){ workItem.typeName = “User Story”; }else { workItem.typeName = nodeHelper.getIssueType(replica.type?.name)?.name ?: “Issue”; } } def defaultUser = nodeHelper.getUserByEmail(“jirasync@comaround.onmicrosoft.com”) workItem.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser workItem.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser workItem.summary = replica.summary workItem.attachments = attachmentHelper.mergeAttachments(workItem, replica) store(issue) def await = { f → scala.concurrent.Await$.MODULE$.result(f, scala.concurrent.duration.Duration.apply(1, java.util.concurrent.TimeUnit.MINUTES)) } def creds = await(httpClient.azureClient.getCredentials()) def issueTrackerUrl = creds.issueTrackerUrl() def project = connection.trackerSettings.fieldValues.“project” def imageReplace = { desc → def _desc = desc def descMatcher = _desc =~ // def matches = descMatcher.size() for (def i=0; i/ /* ​def m = “#https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9adcdd1f7/_apis/wit/attachments/fb31f70e-8e4a-4cc8-b35e-c9a6126a36cf?fileName=image.png#” =~ /#]+)>([^<]+)#</a>/ m.iterator().next()[2] / //if (matchGroup1) //debug.error(“matchGroup0=${matchGroup0} matchGroup1=$matchGroup1”) def attId = issue.attachments.find { a → a.filename == matchGroup1 }?.idStr if (attId) { //$issueTrackerUrl/$organizationName/$projectId/_apis/wit/attachments/$localId?fileName=$attName _desc = _desc.replaceAll(matchGroup0, “”) } } desc } String value = imageReplace(replica.description) if(replica.type?.name==“Bug”){ workItem.“Microsoft.VSTS.TCM.ReproSteps” = value }else{ workItem.description = value } //debug.error(“description = ${workItem.“Microsoft.VSTS.TCM.ReproSteps”} value=${value}”) //workItem.description = WikiToHtml.transform(replica.description) workItem.labels = replica.labels //workItem.labels = replica.labels.collect {it.label = it.label.replace("", " "); it} //workItem.attachments = attachmentHelper.mergeAttachments(workItem, replica) //workItem.iterationPath = replica.customFields.“OTL code”?.value ?: default replica.changedComments = [] //workItem.comments = commentHelper.mergeComments(workItem, replica) workItem.comments = commentHelper.mergeComments(workItem, replica, { c → c.body = imageReplace(c.body) c }) //workItem.comments = replica.comments //debug.error(replica.“Acceptance Criteria”) workItem.“ComAround.Outcome” = imageReplace(replica.“Acceptance Criteria”.value) if(replica.customFields.“Severity”?.value?.value!=“1 - Severe”){ if(replica.customFields.“Severity”?.value?.value==“Medium”){ workItem.“Microsoft.VSTS.Common.Severity” = “3 - Medium” }else{ workItem.“Microsoft.VSTS.Common.Severity” = replica.customFields.“Severity”?.value?.value } }else{ workItem.“Microsoft.VSTS.Common.Severity” = “1 - Critical” } //workItem.“Microsoft.VSTS.Common.Severity” = replica.customFields.“Severity”?.value?.value /**/ if(replica.customFields.“Source”?.value?.value==“Customer”

Here are the relevant rules when syncing description from ADO to Jira:

  1. ADO Out:
if(workItem.type.name=="Bug"){
    replica.description = workItem."Microsoft.VSTS.TCM.ReproSteps"
    //debug.error(replica.description)
}else{
    replica.description = workItem.description
}


  1. Jira In:
HtmlToWiki htw   = new HtmlToWiki()
issue.description  = htw.transform(replica.description)

Whenever a description with HTML like:

From Azure



    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat, sed consectetur magna porttitor.
    


    (Hard return) Nullam a leo ultricies, lobortis turpis a, commodo mauris. Curabitur accumsan, nulla vel aliquam porttitor, neque dolor tempor mi, quis pretium nisi diam sed dui.  
(Soft return) Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat.
    

  

![Image](https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9…ents/1d43effa-dbe9-443b-a8e9-6d9413198143?fileName=image.png)  


    1. Bullet point 1
2. Bullet point 2
3. Bullet point 3



The exalate transformers would try to tackle each html element separately:

HtmlToWiki htw   = new HtmlToWiki(replica.attachments)
issue.description  = htw.transform(replica.description)

Should do something different.

Please, give it a try, and let me know how it goes.
Regards, Serhiy.

Answer by Jose Pablo Alpizar Hidalgo on 19 May 2022

Hi Bhakti Prasad Panda

Thanks for raising this question, in order for you to make sure this works properly please use the following snippets.

Jira Outgoing Sync:

replica.description = nodeHelper.stripHtml(issue.description)

Jira incoming script:

//Place the following lines on top of your code:
import com.exalate.transform.HtmlToWiki
HtmlToWiki htw   = new HtmlToWiki()

//Transforming description from Azure (HTML) to Jira (Wiki)
issue.description  = htw.transform(replica.description)

After verifying the code is correct in your side, please let us know the outcome, thanks for choosing our product for your best interest.

Best regards,
Jose Pablo


Comments:

Bhakti Prasad Panda commented on 19 May 2022

Hello Jose,

I am not sure whether you read my question properly. I suggest please read before answering. We already tried using above script in Jira incoming and its not working properly. Strange thing is it works sometimes and most of the times it doesn’t.

Regards

Bhakti

Ariel Aguilar commented on 19 May 2022

Hi Bhakti,

Isn’t really useful to indicate sometimes it works, and sometimes it doesn’t, can you be more specific what is not working and in what cases? I believe Jose just made sure you are setting up the transformers correctly.

Kind regards,

Ariel

Bhakti Prasad Panda commented on 23 May 2022

Hello Ariel,

When we sync description or any similar field except comments from Azure to Jira, the images in that text gets lost when comes to Jira. Please see the attached image which i added in my question.

So basically it is loosing the image in between. My question is why is it happening?

The code mentioned by Jose is already in use and using only that we are able to sync some part of description.

I hope its clear for you now.

Regards
Bhakti

Serhiy Onyshchenko commented on 29 November 2022

Hey, Bhakti Prasad Panda , I’ll set this up and give you an update this Thursday Dec 1st.

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.