The Exalate team will be on holiday for the coming days - returning Jan 4
Enjoy & stay safe
1
0
-1
1 answer
- 10-1
Here's how one can create comments with properties:
Here's a modified / validated version of the incoming script:if(firstSync){ issue.projectKey = "SD" issue.typeName = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task" } issue.summary = replica.summary issue.description = replica.description issue.attachments = attachmentHelper.mergeAttachments(issue, replica) issue.labels = replica.labels //issue.comments = commentHelper.mergeComments(issue, replica)if (firstSync) { store(issue) } def js = new groovy.json.JsonSlurper() store(issue) def propertyMap = [ "key": "createdByExalate", "value": [ "v" : "true" ] ] def propertylist=[propertyMap] replica.addedComments.each { remoteComment -> def bodyStr = groovy.json.JsonOutput.toJson( [body: remoteComment.body, properties: propertylist] ) //debug.error(""+bodyStr+"--"+issue.key) def res = new JiraClient(httpClient, injector, debug) .http( "POST", "/rest/api/2/issue/${issue.key}/comment", [:], bodyStr, ["Content-Type": ["application/json"]]) { response -> if (response.code >= 400) debug.error("POST /rest/api/2/issue/${issue.key}/comment failed: ${response.body}") else { //store(issue) //debug.error(remoteComment.body+"---"+issue.comments.last()) //debug.error(""+replica.addedComments) def json = js.parseText(response.body) def localCommentId = json.id def trace = new com.exalate.basic.domain.BasicNonPersistentTrace() .setType(com.exalate.api.domain.twintrace.TraceType.COMMENT) .setToSynchronize(true) .setLocalId(localCommentId as String) .setRemoteId(remoteComment.remoteId as String) .setAction(com.exalate.api.domain.twintrace.TraceAction.NONE) traces.add(trace) } } } return new scala.Tuple2(issueKey, scala.collection.JavaConverters.asScalaBuffer(traces)) class JiraClient { // 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 def getGeneralSettings() { def gsp = injector.instanceOf(com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsPersistence.class) def gsOpt = await(gsp.get()) def gs = orNull(gsOpt) gs } private String getJiraCloudUrl() { final def gs = getGeneralSettings() def removeTailingSlash = { String str -> str.trim().replace("/+\$", "") } final def jiraCloudUrl = removeTailingSlash(gs.issueTrackerUrl) jiraCloudUrl } private httpClient private injector private debug 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 } } JiraClient(httpClient, injector, debug) { this.httpClient = httpClient this.injector = injector this.debug = debug } String http(String method, String path, java.util.Map<String, List<String>> queryParams, String body, java.util.Map<String, List<String>> headers) { http(method, path, queryParams, body, headers) { Response response -> if (response.code >= 300) { throw new com.exalate.api.exception.IssueTrackerException("Failed to perform the request $method $path (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 path, java.util.Map<String, List<String>> queryParams, String body, java.util.Map<String, List<String>> headers, Closure<R> transformResponseFn) { def gs = getGeneralSettings() def unsanitizedUrl = jiraCloudUrl + path 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 as java.util.Map<String, List<String>>) m.putAll(queryParams) })() : (queryParams ?: [:] 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 = httpClient .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.libs.ws.WSBodyWritables$.MODULE$.writeableOf_String() request = request.withBody(body, writable) } request = await(httpClient.authenticate( none(), request, gs )) def authEntry = scala.collection.JavaConverters .mapAsJavaMap(request.headers()) .entrySet() .find { kv -> "authorization".equalsIgnoreCase(kv.key) } //debug.error("curl -X ${request.method()} -H '${authEntry.key}: ${authEntry.value.head}' -d '${body}' '${request.url()}${({def qpStr = allQueryParams.inject(""){res, kvs -> res + kvs.collect { k, vs -> vs.collect { v -> "$k=$v" }.join("&"); (qpStr.isEmpty())? "" : "?$qpStr" }}})()}'") response = await(request.execute()) } catch (Exception e) { throw new com.exalate.api.exception.IssueTrackerException("Unable to perform the request $method $path 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 } } }
Regards, Serhiy.
- Charlie Misonne
Hi Serhiy,
Thanks for your answer and that script.
Creating the comments via the REST API is too complex for what I need.
Like you suggested I am now filtrering on the comment author in my automation rules instead of using the customer property.
Still, in the future it might be nice if Exalate could support comment properties without using the REST API.
kind regards,
Charlie
Add your comment...
Jira Cloud has a concept called comment properties which can be used via the REST API and automation for Jira.
Can I set comment properties via Exalate scripts? If I can't, can I create a feature request somewhere?
My use case: I am syncing comments from Azure Devops to Jira sub-tasks.
Once in Jira there is an automation rule copying the comments to the parent issue. But we also have a rule copying the parent comments to the sub-task issues.
Of course I want to avoid duplicate comments. A robust solution would be to add a comment property to keep whether this was a comment originally created by Exalate on a sub-task. Those comments don't have to be syncec back to the sub-task.
Hello, Charlie Misonne thanks for raising this on community!
Aren't all comments by default created via Exalate User?
If so, you may identify comments created by Exalate via author.
worst-case-scenario, it's possible to create comments via Jira's REST API, using
JiraClient
Like so: