4 answers
- 210
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 LepoutreCommentAdd your comment... - 10-1
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.
- Serhiy Onyshchenko
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
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 =~ /<!-- 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 } workItem."Custom.HTML_Field" = processInlineImages( replica."Acceptance Criteria" )
Regards, Serhiy.
- Serhiy Onyshchenko
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.
CommentAdd your comment... - 10-1
Hello Serhiy Onyshchenko ,
Please find the attached zip containing the configuration done on Azure and Jira end:
We will wait for you response.
Regards
Bhakti- Serhiy Onyshchenko
Thanks, Bhakti Prasad Panda , checking the config now to see, what could be going on with those images...
- Serhiy Onyshchenko
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 =~ /<!-- inline image filename=#([^#]+)# -->/ def matches = descMatcher.size() for (def i=0; i<matches; i++) { if (!descMatcher.iterator().hasNext()) { return _desc } def match = descMatcher.iterator().next(); def matchGroup0 = match[0] def matchGroup1 = match[1] def linkMatcher = matchGroup1 =~ /<a href="([^"]+)" ([^\/]+)\/>/ /* 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 attId = issue.attachments.find { a -> a.filename == matchGroup1 }?.idStr if (attId) { //$issueTrackerUrl/$organizationName/$projectId/_apis/wit/attachments/$localId?fileName=$attName _desc = _desc.replaceAll(matchGroup0, "<img src=\"$issueTrackerUrl/${project}/_apis/wit/attachments/${attId}?fileName=$matchGroup1\"/>") } } _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" || 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) */
import com.exalate.transform.HtmlToWiki if(firstSync){ issue.projectKey = "DRHD1" // Set type name from source issue, if not found set a default if(replica.type?.name=="User Story"){ issue.typeName = "Story" }else{ issue.typeName = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Feedback" } } def defaultUser = nodeHelper.getUserByFullName("DBA JIRA sync user") issue.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser issue.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser issue.summary = replica.summary //issue.description = replica.description HtmlToWiki htw = new HtmlToWiki() issue.description = htw.transform(replica.description) //issue.comments = commentHelper.mergeComments(issue, replica){ it.executor = nodeHelper.getUserByEmail(it.author?.email) ?: defaultUser } issue.attachments = attachmentHelper.mergeAttachments(issue, replica) //issue.labels = replica.labels issue.labels = replica.labels.collect { it.label = it.label.trim().replace(" ", "_"); it } issue.comments = commentHelper.mergeComments(issue, replica, { comment -> comment.body = htw.transform(comment.body) }) //debug.error(replica.customKeys."Severity") //issue.customFields."Severity".value = replica.customFields."Severity".value if(replica.customKeys."Severity"!="1 - Critical"){ issue.customFields."Severity".value = replica.customKeys."Severity" }else{ issue.customFields."Severity".value = "1 - Severe" } issue.customFields."Source".value = replica.customFields."Source".value issue.customFields."14357".value = replica.customKeys."Customer" //issue.customFields."Acceptance Criteria".value = replica.customKeys."Outcome" issue.customFields."Acceptance Criteria".value = htw.transform(replica.customKeys."Outcome") if(replica.type?.name=="User Story"){ issue.customFields."Story Points".value = replica.customKeys."Story Points" } //issue.customFields."Story Points".value = replica.customKeys."Story Points" //debug.error(replica.status.name) def statusMapping = [ "New":"Open", "Ready":"Requirements Ready", "Development":"In Progress", "Testing":"Quality Review", "Verified":"Doneness Review", "Implemented":"Doneness Review", "Closed":"Closed" ] def remoteStatusName = replica.status?.name if(replica.status?.name=="New"){ if(issue.status!=null && issue.status.name=="Closed"){ issue.setStatus("Reopened") }else{ if(replica.type?.name=="Bug" || replica.type?.name=="User Story"){ issue.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName) } } }else{ if(replica.type?.name=="Bug" || replica.type?.name=="User Story"){ issue.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName) } } /* 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 */
Here are the relevant rules when syncing description from ADO to Jira:
ADO Out:
if(workItem.type.name=="Bug"){ replica.description = workItem."Microsoft.VSTS.TCM.ReproSteps" //debug.error(replica.description) }else{ replica.description = workItem.description }
Jira In:
HtmlToWiki htw = new HtmlToWiki() issue.description = htw.transform(replica.description)
Whenever a description with HTML like:
<div><p style="box-sizing:border-box;margin:0px;color:rgba(0, 0, 0, 0.9);"><span style="box-sizing:border-box;">From Azure</span> </p> <p style="box-sizing:border-box;margin:0px;color:rgba(0, 0, 0, 0.9);"><span style="box-sizing:border-box;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat, sed consectetur magna porttitor.</span> </p> <p style="box-sizing:border-box;margin:0px;color:rgba(0, 0, 0, 0.9);"><strong style="">(Hard return)</strong><span style=""> 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.<br></span><strong style="">(Soft return)</strong><span style=""> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae semper augue. Integer euismod justo in dolor consequat.</span> </p><br></div> <div><img src="https://dev.azure.com/comaround/0006628f-6b9d-4174-a800-78f9…ents/1d43effa-dbe9-443b-a8e9-6d9413198143?fileName=image.png" alt=Image><br></div> <div> <ol style="box-sizing:border-box;margin:0px;padding-left:40px;color:rgba(0, 0, 0, 0.9);"> <li style="box-sizing:border-box;">Bullet point 1</li> <li style="box-sizing:border-box;">Bullet point 2</li> <li style="box-sizing:border-box;">Bullet point 3</li> </ol> </div>
The exalate transformers would try to tackle each html element separately:
each<p>
will result in a line break after the text inside it
each<ol>
will give a #-based list
and <img> would simply be ignored.
Luckily, this article https://community.servicenow.com/community?id=community_article&sys_id=5566161fdb3264103daa1ea668961981
suggests thatHtmlToWiki 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.
CommentAdd your comment... - 10-1
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- Bhakti Prasad Panda
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
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
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
Hey, Bhakti Prasad Panda , I'll set this up and give you an update this Thursday Dec 1st.
CommentAdd your comment...
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?