Jira Cloud <-> Zendesk Sync comments and Description with formatting

Introduction
Jira Cloud comment supports formatting like bold, italic, underline , colour fonts and many more. To sync this to Zendesk is not totally supported by Exalate. The reason being, Jira cloud is using Mark Up language where Zendesk is using Markdown and HTML both. Exalate has default methods and scripts to convert into Markdown but that has some limitation like it will not support colour fonts , images and few more.

To achieve this , here is the advance script which helps you to create comment with html content in Zendesk.

Also i added how we can sync comments and description inline images from zendesk to Jira cloud

The code:

Jira Cloud Outgoing Sync Script

replica.comments = nodeHelper.getHtmlComments(issue)
replica.description    = nodeHelper.getHtmlField(issue,"description")

Zendesk Incoming Sync Script

issue.labels = replica.labels
issue.summary = replica.summary
def imgInDesc=false
def replicaDesc=replica.description
if(firstSync){
if(replicaDesc){
  if(replicaDesc.contains("img src")){
imgInDesc=true
  }
replicaDesc=replicaDesc.replaceAll("<ins>", "<u>").replaceAll("</ins>", "</u>")
String htmlDesc=replicaDesc.replaceAll(/<img src="\/attachments\/(\d+)\?name=([^"]+)" alt="([^"]+)">/) { fullMatch, id, imageName, altText ->
  return ""
}
//debug.error(""+htmlDesc)
  issue.description= nodeHelper.toMarkDownFromHtml(htmlDesc)
}
else{
  issue.description= "No description"
}
}
 
issue.attachments = attachmentHelper.mergeAttachments(issue, replica)
 
  store(issue)
   
 def jsonSlurper = new groovy.json.JsonSlurper()
   def attachmentsMap = [:]     
 httpClient.http(
        "GET",
        "/api/v2/tickets/${issue.key}/comments",
        null,
        [:],
        ["Accept": ["application/json"], "Content-type": ["application/json"]])   
        {   
          req,response2->
          if(response2.code >= 400) {
 
            throw new com.exalate.api.exception.IssueTrackerException("1 Failed to get comments with status code  " + response2.code + " and with details " + response2.body)
          }
          else {
          def commentResponse = response2.body
          if(commentResponse?.comments?.size()>0)
          {
            commentResponse.comments.each { comment ->
            if (comment.attachments) { // Check if attachments exist
            comment.attachments.each { attachment ->
            attachmentsMap[attachment.file_name] = attachment.content_url
              }
              }
            }
          }
          }
        }
//for all comments  
if(replicaDesc && imgInDesc)
  {
def regex= /<img src="\/attachments\/(\d+)\?name=([^"]+)" alt="([^"]+)">/
def matches = []
def allMatches = replicaDesc.findAll(regex)
allMatches.each { match ->
// In this case, 'match' will be the captured `src` attribute
matches << match
}
 
def imgMatches=[]
imgMatches << "desc images:"
  matches.each { src ->
def img1=src.replaceAll(/<img src="\/attachments\/(\d+)\?name=([^"]+)" alt="([^"]+)">/) { fullMatch, id, imageName, altText ->
String newToken = attachmentsMap[imageName]
if (newToken) {
    return """<img src="${newToken}" alt="${altText}" />"""
} else {
    return fullMatch // return original if no mapping found
}
  }
imgMatches << img1
  }
def htmlExtraComment=imgMatches.join("\n")
//debug.error(imgMatches)
   def json = new groovy.json.JsonBuilder()
json.ticket {
comment {
    html_body htmlExtraComment
}
}
def jsonString = json.toString()
 httpClient.http("PUT","/api/v2/tickets/${issue.key}.json",jsonString,[:],["Accept": ["application/json"], "Content-type": ["application/json"]])
 {
req,response->
 
    if (response.code >= 400) {
 
      throw new com.exalate.api.exception.IssueTrackerException("Failed to create html comment with status code  " + response.code + " and with details " + response.body)
    }
 }

  }        
replica.addedComments.each
{
  it ->
def commentBody = it.body
  //String jsonString =
def json = new groovy.json.JsonBuilder()
commentBody = commentBody.replaceAll("<ins>", "<u>").replaceAll("</ins>", "</u>")
String updatedHtmlContent = commentBody.replaceAll(/<img src="\/attachments\/(\d+)\?name=([^"]+)" alt="([^"]+)">/) { fullMatch, id, imageName, altText ->
  //  debug.error(""+imageName)
String newToken = attachmentsMap[imageName]
if (newToken) {
    return """<img src="${newToken}" alt="${altText}" />"""
} else {
    return fullMatch // return original if no mapping found
}
}
 
//debug.error(""+updatedHtmlContent)
 
json.ticket {
comment {
    html_body updatedHtmlContent
}
}
 
def jsonString = json.toString()
 httpClient.http("PUT","/api/v2/tickets/${issue.key}.json",jsonString,[:],["Accept": ["application/json"], "Content-type": ["application/json"]])
 {
req,response->
 
    if (response.code >= 400) {
 
      throw new com.exalate.api.exception.IssueTrackerException("Failed to create html comment with status code  " + response.code + " and with details " + response.body)
    }
  else {
    def response3=httpClient.http("GET",
        "/api/v2/tickets/${issue.key}/comments",
        null,
        [:],
        ["Accept": ["application/json"], "Content-type": ["application/json"]]
      ){
        rer2,response3->
      if (response3.code >= 400) {
 
            throw new com.exalate.api.exception.IssueTrackerException("Failed to get comments with status code  " + response3.code + " and with details " + response3.body)
          }
        else {
        //store(issue)
          def commentResponse = response3.body
 
          // Assuming you want to find the latest comment by the highest ID
          if(commentResponse?.comments?.size()>0)
          {
          def latestComment = commentResponse.comments[commentResponse.comments.size()-1]
          def commentId=latestComment.id?:null
         // debug.error(""+commentId)
          if (commentId) {
            def trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
              .setType(com.exalate.api.domain.twintrace.TraceType.COMMENT)
              .setToSynchronize(true)
              .setLocalId(commentId as String)
              .setRemoteId(it.remoteId as String)
              .setAction(com.exalate.api.domain.twintrace.TraceAction.NONE)
            traces.add(trace)
            Thread.sleep(5000)
          //  if(it.remoteId=="10820")
             log.error(""+trace+" \n all:"+traces)
          }
          }
          //    debug.error(""+comments)
        }
      }
      }
 
  }
}
 
 
 
 
return new scala.Tuple2(issueKey, scala.collection.JavaConverters.asScalaBuffer(traces))

Now, because we are adding traces manually , as soon as Exalate adds the traces , Zendesk fetches that last synced comment and sync back to Jira cloud , to avoid this we will filter the author in outgoing sync script.

Note: This will not work in case we impersonate the Zendesk comment

Zendesk Outgoing Sync Script

replica.description    = nodeHelper.getHtmlDescription(issue)
def allcomments=nodeHelper.getHtmlComments(issue)
def replicaComment=[]
allcomments.each{
  it->
  if(it.author.displayName != "Proxy User Name")
  {
replicaComment << it
  }
}
replica.comments = replicaComment

Jira Cloud Incoming Sync Script

if (firstSync) {
issue.projectKey  = "SOURCE"
// Set the same issue type as the source issue. If not found, set a default.
issue.typeName    = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "New Feature"
issue.description  = nodeHelper.toMarkDownFromHtml(replica.description)

}
issue.summary      = replica.summary
issue.attachments  = attachmentHelper.mergeAttachments(issue, replica)
issue.labels       = replica.labels
issue.comments     = nodeHelper.toMarkDownComments(commentHelper.mergeComments(issue, replica))