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.