Need a solution for a bi-directional sync, of embedded (in-line) images in the description/comments/Text fields etc.) and their attachments

Originally asked by Harold Oconitrillo on 14 September 2023 (original question)


Hello support.

Main goal is to be able to create a bi-directional sync. from Azure Devops Board to Jira.

Here the first issues that I have so far:

Context: using Exalate in Script mode From Azure Work item(Outgoing sync ) to Jira Cloud Issue (Incoming sync) ( note: as a start we need Azure to Jira sync. but our main goal is to have bi-directional sync )

To reproduce my situation:

  • From Azure Board create a work item
    • in the Description add some text
      • Add an image by pasting from a print screen
      • Save
    • Reopen the same work item
    • add a Comment
      • Add an new image by pasting from a new print screen
      • Save

Azure DevOps Board - Exalate outbound

// Azure DevOps Board - Exalate outbound

replica.description    = workItem.description	// Note description will contain HTML

replica.attachments    = workItem.attachments

replica.comments       = workItem.comments 		// Note comment will contain HTML

NOTE from the Jira Exalate connection information of the Remote replica entity section about the Attachments

"attachments": [
      {
        "id": "5729801f-8a84-4dd6-adc7-7521854a47d2",
        "filename": "image.png",
        "internal": false,
        "zip": false
      },
      {
        "id": "fbc509af-2794-4766-b347-99a7fd7bd688",
        "filename": "image.png",
        "internal": false,
        "zip": false
      }
    ],

Jira Cloud - Exalate Incoming sync

// Jira Cloud - Exalate Incoming sync

import com.exalate.transform.HtmlToWiki


issue.attachments  = attachmentHelper.mergeAttachments(issue, replica)

HtmlToWiki htwDesc   = new HtmlToWiki(replica.attachments)
String wikiDescriptionText  = htwDesc.transform(replica.description)  // replica as HTML Jira understands wiki
issue.description = wikiDescriptionText 


HtmlToWiki htwComment   = new HtmlToWiki(replica.attachments) //  // replica as HTML Jira understands wiki 
issue.comments = commentHelper.mergeComments(issue, replica,  {
    comment ->    
    comment.body = htwComment.transform(comment.body)
    comment
})

The result in the Jira issue, both the description and comment text fields have the same image (The last image added to the comment, the second image.png).

I did try the suggested solution from : https://community.exalate.com/display/exacom/questions/20124163/exalating-attachments-pasted-images-on-azuredevops-result-in-same-name-files-on-receiving-jira-side

Solution to rename the filename values in replica.attachments but the HtmlToWiki::processImage methode/function base it self on the HTML content only to scans for filename in the Image ‘src’ attribute. Giving me a broken in-line image link on the Jira side.

Since the Field content (HTML) (description and/or comments) in-line images information need/must fit the attachment information in order to resolve the image information to display in the proper context. There is a missing part to the solution.

Hypotheses:

So by renaming the images from azure to Jira …in Jira new filename ….when will do the other sync Jira to Azure inbound script even using the attachmentHelper.mergeAttachments tool … are those images are added to the work item making duplicates because their is different filenames.

Main goal is to be able to create a bi-directional sync. from

  • Azure Devops Board
  • Jira Cloud

Help would be appreciated,


Answer by Javier Pozuelo on 26 September 2023

Hello,

Add the following code on your Outgoing Sync on Jira Cloud

replica.comments = nodeHelper.getHtmlComments(issue)

Incoming Sync Azure DevOps

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)


Kind Regards,

Javier Pozuelo


I’m experiencing an issue with Exalate when syncing images from Azure DevOps to Jira. All images appear as “image.png” instead of retaining their original names.

Has anyone encountered this issue before? Any suggestions on how to resolve it?

Thanks in advance!