1
0
-1

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.

  1. Serhiy Onyshchenko

    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: 

    //issue.comments = commentHelper.mergeComments(issue, replica)if (firstSync) { store(issue) }
    def jc = new JiraClient(httpClient)
    def js = new groovy.json.JsonSlurper()
    replica.addedComments.each { remoteComment ->
      def bodyStr = groovy.json.JsonOutput.toJson(
        [body:remoteComment.body, properties:[[key:"createdByExalate", "value":true]]]
      )
      def result = jc.http("POST", "/rest/api/2/issue/${issue.idStr}/comment", [:], bodyStr, ["Content-Type":"application/json"]) { response -> 
        if(response.code >= 300) debug.error("Failed to create comment with properties: ${response.body}")
        else {
          def json = js.parseText(response.body)
          json
        }
      }
      def localCommentId = result.id  def trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
         .setType(com.exalate.api.domain.twintrace.TraceType.COMMENT)
         .setToSynchronize(true)
         .setLocalId(localCommentId)
         .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))
CommentAdd your comment...

1 answer

  1.  
    2
    1
    0

    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.

    1. 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

    CommentAdd your comment...