Questions
1
0
-1
2 answers
- 210
High level view of the solution:
- ADO to read the 7pace data and add it to the replica to send to Jira
- Jira to read the time information sent by ADO and populate it within Tempo.
Step 1:
- Choose an endpoint to use in order to fetch data from 7pace.
- Add the GroovyHttpClient class to your ADO outgoing code:GroovyHttpClient 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 } } }
Run the 7pace API call using the GroovyHttpClient e.g.:
Usage Expand sourcedef 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 <<insert access_token here>>"] ] ) { response -> if (response.code >= 400) throw new com.exalate.api.exception.IssueTrackerException("Failed") else response.body as String }
Parse the received response to fetch the data you want e.g. I am picking up the PeriodLength here only, and add it to the replica. We are also filtering the received data by workItemId in order to get relevant data only:
Parse the response Expand sourcedef 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()) replica.customKeys."PeriodLength" += json.value[i]?.PeriodLength }
Step 2:
On the Jira incoming script, receive the time information from ADO and use the addWorkLog() method to populate it into Tempo:
Jira Incoming Expand sourceif (firstSync) issue.workLogs = workLogHelper.addWorkLog("2022/08/11", "${((int) (replica.customKeys.'PeriodLength')/60)}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) }
Please review the attached video to see how the solution works.
Thanks
Majid
p.s.: Some more reading on the subject is here.
CommentAdd your comment... - 10-1
The full code snippets on source and destination are here:
Jira Outgoing Expand sourcereplica.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 sourceif(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 sourceclass 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 sourceworkItem.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) */
CommentAdd your comment...
We would like to send ADO workitems to Jira issues.
Any time tracking information contained in the 7pace time tracker (ADO extension) for that workitem must also be sent over to the Tempo (Jira addon) on the relevant Jira issue.