Answer by Syed Majid Hassan on 12 August 2022
The full code snippets on source and destination are here:
Jira Outgoing Expand source
replica.key = issue.key
replica.type = issue.type
replica.assignee = issue.assignee
replica.reporter = issue.reporter
replica.summary = issue.summary
replica.description = 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 = []
/*
Custom Fields
replica.customFields."CF Name" = issue.customFields."CF Name"
*/
Jira Incoming Expand source
if(firstSync){
issue.projectKey = "CM"
issue.typeName = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"
}
issue.summary = replica.summary
issue.description = replica.description
issue.comments = commentHelper.mergeComments(issue, replica)
issue.attachments = attachmentHelper.mergeAttachments(issue, replica)
issue.labels = replica.labels
if (firstSync)
issue.workLogs = workLogHelper.addWorkLog("2022/08/11", "${((int) (replica.customKeys.'PeriodLength'))}m", "Test", issue.workLogs)
else{
if (previous.customKeys.'PeriodLength' != 0)
issue.workLogs = workLogHelper.addWorkLog("2022/08/11", "${((((int) (replica.customKeys.'PeriodLength')) - ((int) previous.customKeys.'PeriodLength'))/60)}m", "Test", issue.workLogs)
else
issue.workLogs = workLogHelper.addWorkLog("2022/08/11", "${((int) (replica.customKeys.'PeriodLength')/60)}m", "Test", issue.workLogs)
}
ADO Outgoing Expand source
class GroovyHttpClient {
// SCALA HELPERS
private static <T> T await(scala.concurrent.Future<T> f) { scala.concurrent.Await$.MODULE$.result(f, scala.concurrent.duration.Duration$.MODULE$.Inf()) }
private static <T> T orNull(scala.Option<T> opt) { opt.isDefined() ? opt.get() : null }
private static <T> scala.Option<T> none() { scala.Option$.MODULE$.<T>empty() }
@SuppressWarnings("GroovyUnusedDeclaration")
private static <T> scala.Option<T> none(Class<T> evidence) { scala.Option$.MODULE$.<T>empty() }
private static <L, R> scala.Tuple2<L, R> pair(L l, R r) { scala.Tuple2$.MODULE$.<L, R>apply(l, r) }
// SERVICES AND EXALATE API
private httpClient
def parseQueryString = { String string ->
string.split('&').collectEntries{ param ->
param.split('=', 2).collect{ URLDecoder.decode(it, 'UTF-8') }
}
}
//Usage examples: https://gist.github.com/treyturner/4c0f609677cbab7cef9f
def parseUri
{
parseUri = { String uri ->
def parsedUri
try {
parsedUri = new URI(uri)
if (parsedUri.scheme == 'mailto') {
def schemeSpecificPartList = parsedUri.schemeSpecificPart.split('\\?', 2)
def tempMailMap = parseQueryString(schemeSpecificPartList[1])
parsedUri.metaClass.mailMap = [
recipient: schemeSpecificPartList[0],
cc : tempMailMap.find { it.key.toLowerCase() == 'cc' }.value,
bcc : tempMailMap.find { it.key.toLowerCase() == 'bcc' }.value,
subject : tempMailMap.find { it.key.toLowerCase() == 'subject' }.value,
body : tempMailMap.find { it.key.toLowerCase() == 'body' }.value
]
}
if (parsedUri.fragment?.contains('?')) { // handle both fragment and query string
parsedUri.metaClass.rawQuery = parsedUri.rawFragment.split('\\?')[1]
parsedUri.metaClass.query = parsedUri.fragment.split('\\?')[1]
parsedUri.metaClass.rawFragment = parsedUri.rawFragment.split('\\?')[0]
parsedUri.metaClass.fragment = parsedUri.fragment.split('\\?')[0]
}
if (parsedUri.rawQuery) {
parsedUri.metaClass.queryMap = parseQueryString(parsedUri.rawQuery)
} else {
parsedUri.metaClass.queryMap = null
}
if (parsedUri.queryMap) {
parsedUri.queryMap.keySet().each { key ->
def value = parsedUri.queryMap[key]
if (value.startsWith('http') || value.startsWith('/')) {
parsedUri.queryMap[key] = parseUri(value)
}
}
}
} catch (e) {
throw new com.exalate.api.exception.IssueTrackerException("Parsing of URI failed: $uri\n$e", e)
}
parsedUri
}
}
GroovyHttpClient(httpClient) {
this.httpClient = httpClient
}
String http(String method, String url, String body, java.util.Map<String, List<String>> headers) {
http(method, url, body, headers) { Response response ->
if (response.code >= 300) {
throw new com.exalate.api.exception.IssueTrackerException("Failed to perform the request $method $url (status ${response.code}), and body was: \n```$body```\nPlease contact Exalate Support: ".toString() + response.body)
}
response.body as String
}
}
public <R> R http(String method, String _url, String body, java.util.Map<String, List<String>> headers, Closure<R> transformResponseFn) {
def unsanitizedUrl = _url
def parsedUri = parseUri(unsanitizedUrl)
def embeddedQueryParams = parsedUri.queryMap
def allQueryParams = embeddedQueryParams instanceof java.util.Map ?
({
def m = [:] as java.util.Map<String, List<String>>;
m.putAll(embeddedQueryParams.collectEntries { k, v -> [k, [v]] } as java.util.Map<String, List<String>>)
m
})()
: ([:] as java.util.Map<String, List<String>>)
def urlWithoutQueryParams = { String url ->
URI uri = new URI(url)
new URI(uri.getScheme(),
uri.getUserInfo(), uri.getHost(), uri.getPort(),
uri.getPath(),
null, // Ignore the query part of the input url
uri.getFragment()).toString()
}
def sanitizedUrl = urlWithoutQueryParams(unsanitizedUrl)
def response
try {
def request = ({
try { httpClient.azureClient }
catch (e) { httpClient.issueTrackerClient }
})()
.ws
.url(sanitizedUrl)
.withMethod(method)
if (headers != null && !headers?.isEmpty()) {
def scalaHeaders = scala.collection.JavaConversions.asScalaBuffer(
headers?.entrySet().inject([] as List<scala.Tuple2<String, String>>) { List<scala.Tuple2<String, String>> result, kv ->
kv.value.each { v -> result.add(pair(kv.key, v) as scala.Tuple2<String, String>) }
result
}
)
request = request.withHeaders(scalaHeaders)
}
if (!allQueryParams?.isEmpty()) {
def scalaQueryParams = scala.collection.JavaConversions.asScalaBuffer(allQueryParams?.entrySet().inject([] as List<scala.Tuple2<String, String>>) { List<scala.Tuple2<String, String>> result, kv ->
kv.value.each { v -> result.add(pair(kv.key, v) as scala.Tuple2<String, String>) }
result
})
request = request.withQueryString(scalaQueryParams)
}
if (body != null) {
def writable = play.api.http.Writeable$.MODULE$.wString(play.api.mvc.Codec.utf_8())
request = request.withBody(body, writable)
}
response = await(
request.execute()
)
} catch (Exception e) {
throw new com.exalate.api.exception.IssueTrackerException("Unable to perform the request $method $_url with body: \n```$body```\n, please contact Exalate Support: ".toString() + e.message, e)
}
java.util.Map<String, List<String>> javaMap = [:]
for (scala.Tuple2<String, scala.collection.Seq<String>> headerTuple : scala.collection.JavaConverters.bufferAsJavaListConverter(response.allHeaders().toBuffer()).asJava()) {
def javaList = []
javaList.addAll(scala.collection.JavaConverters.bufferAsJavaListConverter(headerTuple._2().toBuffer()).asJava())
javaMap[headerTuple._1()] = javaList
}
def javaResponse = new Response(response.body(), new Integer(response.status()), javaMap)
return transformResponseFn(javaResponse)
}
public static class Response {
final String body
final Integer code
final java.util.Map<String, List<String>> headers
Response(String body, Integer code, java.util.Map<String, List<String>> headers) {
this.body = body
this.code = code
this.headers = headers
}
}
}
replica.key = workItem.key
replica.assignee = workItem.assignee
replica.summary = workItem.summary
replica.description = nodeHelper.stripHtml(workItem.description)
replica.type = workItem.type
replica.status = workItem.status
replica.labels = workItem.labels
replica.priority = workItem.priority
replica.comments = nodeHelper.stripHtmlFromComments(workItem.comments)
replica.attachments = workItem.attachments
replica.project = workItem.project
replica.areaPath = workItem.areaPath
replica.iterationPath = workItem.iterationPath
def res = new GroovyHttpClient(httpClient)
.http(
"GET",
"https://exalatedemo.timehub.7pace.com/api/odata/v3.1/workLogsOnly",
null,
["Accept": ["application/json"], "Content-type" : ["application/json"], "Authorization":["Basic cm9tYW4ubWVsbnljaGVua29AaWRhbGtvLmNvbTp4dzZVVGIyTV8zdngzd0FqMWt3aVpLSVpjYkN5NjZNbGE0Q2hHLWU1Wl9v"] ]
)
{
response ->
if (response.code >= 400) {
throw new com.exalate.api.exception.IssueTrackerException("Failed to get orgnaizations with name ")
} else response.body as String
}
def js = new groovy.json.JsonSlurper()
def json = js.parseText(res)
replica.customKeys."PeriodLength" = 0
for(int i=0; i<json.value.size();i++){
if (json.value[i].WorkItemId.toString() == workItem.key.toString())
//debug.error("${workItem.key}")
replica.customKeys."PeriodLength" += json.value[i]?.PeriodLength
}
ADO Incoming Expand source
workItem.labels = replica.labels
workItem.priority = replica.priority
if(firstSync){
// Set type name from source entity, if not found set a default
workItem.typeName = nodeHelper.getIssueType(replica.type?.name)?.name ?: "Task";
}
workItem.summary = replica.summary
workItem.description = replica.description
workItem.attachments = attachmentHelper.mergeAttachments(workItem, replica)
workItem.comments = commentHelper.mergeComments(workItem, replica)
/*
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)
*/