Note

We've packed up and moved from Confluence to Discourse to bring you a better, more interactive space. Out of courtesy we didn't migrate your user account so - you will have to signup again

The Exalate team will be on holiday for the coming days - returning Jan 4
Enjoy & stay safe

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Introduction

Hello all,


With this tutorial I would like to show you how we can syncronize I'm excited to present this tutorial, where we will explore the seamless synchronization of Tempo Worklogs between two Jira Cloud instances using Exalate. 


To set the stage, let's consider a scenario where we have Jira Cloud instance When Exalating a ticket from Jira Cloud A (JCA) to and Jira Cloud B, it will get populated in JCBinstance B (JCB). When a user adds time in the ticket in ticket is exalated from JCA to JCB, it needs to get reflected on JCA. This needs to happen visa-versa.

Tempo Worklogs

After installing Tempo Worklogs, you will see it getting added under your apps selection. 

Exalate uses Tempo Cloud REST API to get access to the tempo worklogs.

Generate Access Token

Exalate requires access to Tempo. To grant secure, temporary access to Tempo you need to create a user access token. This access token is based on current permissions of the Jira Cloud user.

Required permissions

The user who generated the access token must have the following permissions:

  • Jira permissions:
    • Create worklogs
    • View all worklogs
    • Log work for others
    • Work on issues
  • Tempo permissions:
    • view team worklogs
    • manage team worklogs

Generate the Access Token under Tempo settings - API integration tab in your Jira Cloud settings.

The Scripts

Jira Cloud A

Outgoing Sync Jira Cloud A

Code Block
languagegroovy
titleOutgoing Sync
linenumberstrue
replica.workLogs = issue.workLogs
TempoWorkLogSync.send(
	"dJWtvBhJUkoHroDcEd8iYyYfnd0Bm",  // replace the "token" with the previously generated access token
	replica, 
	issue, 
	httpClient, 
	nodeHelper
) 

Incoming Sync Jira Cloud A

Add the imports in the beginning of the code.

Add the functions at the end of the code.

Code Block
languagegroovy
titleIncoming Sync
linenumberstrue
collapsetrue
import com.exalate.api.domain.webhook.WebhookEntityType
import com.exalate.basic.domain.hubobject.v1.BasicHubIssue
import com.exalate.basic.domain.hubobject.v1.BasicHubUser
import com.exalate.basic.domain.hubobject.v1.BasicHubWorkLog
import com.exalate.replication.services.replication.impersonation.AuditLogService
import org.slf4j.LoggerFactory
import org.slf4j.Logger
import play.libs.Json
import scala.concurrent.Await$;
import scala.concurrent.duration.Duration$;
import java.text.SimpleDateFormat
import java.time.Instant

//Your normal Incoming Sync code
//Add these functions at the end

Worklogs.receive(
	  "dJWtvBhJUkoHroDcEd8iYyYfnd0Bm",  // replace the "token" with the previously generated access token
	replica,  
	  replica,
	  issue,
	  httpClient,
	  traces,
	  nodeHelper
)


class Worklogs {

    
 


    static receive(String token, BasicHubIssue replica, BasicHubIssue issue, httpClient, traces, nodeHelper){
        receive(
                token,
                replica,
                issue,
                httpClient,
                traces,
                nodeHelper,
                { BasicHubWorkLog w ->
                    def getUser = { String key ->
                        def localAuthor = nodeHelper.getUser(key)
                        if (localAuthor == null) {
                            localAuthor = new BasicHubUser()
                            localAuthor.key = "557058:c020323a-70e4-4c07-9ccc-3ad89b1c02ec"
                        }
                        localAuthor
                    }
                    w.author = w.author ? getUser(w.author.key) : null
                    w.updateAuthor = w.updateAuthor ? getUser(w.updateAuthor.key) : null
                    w
                }
        )
    }
   
   
    static receive(String token, BasicHubIssue replica, BasicHubIssue issue, httpClient, traces, nodeHelper, Closure<?> onWorklogFn){
        def http = { String method, String path, Map<String, List<String>> queryParams, String body, Map<String, List<String>> headers ->

            def await = { future -> Await$.MODULE$.result(future, Duration$.MODULE$.apply(60, java.util.concurrent.TimeUnit.SECONDS)) }
            def orNull = { scala.Option<?> opt -> opt.isDefined() ? opt.get() : null }
            def pair = { l, r -> scala.Tuple2$.MODULE$.<?, ?>apply(l, r) }
            def none = { scala.Option$.MODULE$.<?> empty() }

            def getGeneralSettings = {
                def classLoader = this.getClassLoader()
                def gsp
                try {
                    gsp = InjectorGetter.getInjector().instanceOf(classLoader.loadClass("com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsRepository"))
                } catch(ClassNotFoundException exception) {
                    gsp = InjectorGetter.getInjector().instanceOf(classLoader.loadClass("com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsPersistence"))
                }
                def gsOpt = await(gsp.get())
                def gs = orNull(gsOpt)
                gs
            }
            final def gs = getGeneralSettings()

            def removeTailingSlash = { String str -> str.trim().replace("/+\$", "") }
            final def tempoRestApiUrl = "https://api.tempo.io/core/3"

            def parseQueryString = { String string ->
                string.split('&').collectEntries{ param ->
                    param.split('=', 2).collect{ URLDecoder.decode(it, 'UTF-8') }
                }
            }

            
            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])
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.mailMap = [
                                recipient: schemeSpecificPartList[0],
                                cc       : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'cc' }.value,
                                bcc      : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'bcc' }.value,
                                subject  : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'subject' }.value,
                                body     : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'body' }.value
                        ]
                    }
                    if (parsedUri.fragment?.contains('?')) { 
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.rawQuery = parsedUri.rawFragment.split('\\?')[1]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.query = parsedUri.fragment.split('\\?')[1]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.rawFragment = parsedUri.rawFragment.split('\\?')[0]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.fragment = parsedUri.fragment.split('\\?')[0]
                    }
                    if (parsedUri.rawQuery) {
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.queryMap = parseQueryString(parsedUri.rawQuery)
                    } else {
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.queryMap = null
                    }

                    //noinspection GrUnresolvedAccess
                    if (parsedUri.queryMap) {
                        //noinspection GrUnresolvedAccess
                        parsedUri.queryMap.keySet().each { key ->
                            def value = parsedUri.queryMap[key]
                            //noinspection GrUnresolvedAccess
                            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
            }

            def unsanitizedUrl = tempoRestApiUrl + path
            def parsedUri = parseUri(unsanitizedUrl)

       
            def embeddedQueryParams = parsedUri.queryMap

            def allQueryParams = embeddedQueryParams instanceof Map ?
                    ({
                        def m = [:] as Map<String, List<String>>;
                        m.putAll(embeddedQueryParams as Map<String, List<String>>)
                        m.putAll(queryParams)
                    })()
                    : (queryParams ?: [:] as 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, 
                        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(queryParams.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)
                }
                response = await(request.execute())


            } catch (Exception e) {
                throw new com.exalate.api.exception.IssueTrackerException("Unable to perform the request $method $path, please contact Exalate Support: ".toString() + e.message, e)
            }
            if (response.status() >= 300) {
                throw new com.exalate.api.exception.IssueTrackerException("Failed to perform the request $method $path ${body ? "with body `$body`".toString() : ""}(status ${response.status()}), please contact Exalate Support: ".toString() + response.body())
            }
            response.body() as String
        }

        def gsp = InjectorGetter.getInjector().instanceOf(AuditLogService.class)

        def js = new groovy.json.JsonSlurper()
        def jo = new groovy.json.JsonOutput()

        def listAdditionalParams = replica.customKeys."tempoWorklogParams" as Map<String, Map<String, Object>>;
        
        replica.workLogs.findAll{it.id == null}.each{ BasicHubWorkLog worklog ->
            def transformedWorklog
            
          
            try {
                transformedWorklog = onWorklogFn(worklog)
            } catch (com.exalate.api.exception.IssueTrackerException ite) {
                throw ite
            } catch (Exception e) {
                throw new com.exalate.api.exception.IssueTrackerException(e)
            }
            if (transformedWorklog instanceof BasicHubWorkLog) {
                worklog = transformedWorklog as BasicHubWorkLog
            } else if (transformedWorklog == null) {
                return
            }

            def auditLogOpt = gsp.createAuditLog(scala.Option$.MODULE$.<String>apply(issue.id as String),
                    WebhookEntityType.WORKLOG_CREATED,
                    worklog.getAuthor().getKey()
            )

            def attributes = ((listAdditionalParams?.get(worklog.remoteId.toString())?.get("attributes") as Map<String, Object>)?.get("values") as List<Map<String, String>>)?.inject([]){
                List<Map<String, String>>result, Map<String, String> attribute ->
                    result.add(
                            [
                                    key: attribute.get("key"),
                                    value: attribute.get("value")
                            ]
                    )
                    result
            } ?: []
            def properties = [
                    issueKey : issue.key,
                    timeSpentSeconds : worklog.getTimeSpent(),
                    billableSeconds: listAdditionalParams?.get(worklog.remoteId.toString())?.get("billableSeconds") ?: worklog.getTimeSpent(),
                    startDate : new java.text.SimpleDateFormat("yyyy-MM-dd").format(worklog.startDate),
                    startTime : new java.text.SimpleDateFormat("hh:mm:ss").format(worklog.startDate) ?: listAdditionalParams?.get(worklog.remoteId.toString())?.get("startTime"),//strDateSplitted[1].split("\\.")[0],
                    description : worklog.getComment(),
                    authorAccountId : worklog.getAuthor().getKey(),
                    remainingEstimateSeconds:  replica.remainingEstimate ?: listAdditionalParams?.get(worklog.remoteId.toString())?.get("remainingEstimateSeconds"),
                    attributes : attributes
            ]
            def jsonTempoPost = jo.toJson(properties)


            def response = js.parseText(http(
                    "POST",
                    "/worklogs",
                    null,
                    jsonTempoPost,
                    [
                            "Authorization":["Bearer ${token}".toString()],
                            "Content-Type":["application/json"],
                    ]
            ))
            println(response)

            gsp.updateAuditLog(scala.Option$.MODULE$.apply(auditLogOpt), issue.id as String, response["jiraWorklogId"] as String, Json.stringify(Json.toJson(response)))


            String localIdStr = response["tempoWorklogId"]
            String remoteIdStr = worklog.remoteId.toString()

            com.exalate.api.domain.twintrace.INonPersistentTrace trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
                    .setLocalId(localIdStr)
                    .setRemoteId(remoteIdStr)
                    .setType(com.exalate.api.domain.twintrace.TraceType.WORKLOG)
                    .setAction(com.exalate.api.domain.twintrace.TraceAction.NONE)
                    .setToSynchronize(true)
            traces.add(trace)
        }



    }

    
}


Jira Cloud B

Outgoing Sync Jira Cloud B

Code Block
languagegroovy
titleOutgoing Sync
linenumberstrue
replica.workLogs = issue.workLogs
TempoWorkLogSync.send(
	"B0V4tQJ22LhPnznllWoT2s0N29So5",  // replace the "token" with the previously generated access token
	replica, 
	issue, 
	httpClient, 
	nodeHelper
)  

Incoming Sync Jira Cloud B

Add the imports in the beginning of the code.

Add the functions at the end of the code.

Code Block
languagegroovy
titleIncoming Sync
linenumberstrue
collapsetrue
import com.exalate.api.domain.webhook.WebhookEntityType
import com.exalate.basic.domain.hubobject.v1.BasicHubIssue
import com.exalate.basic.domain.hubobject.v1.BasicHubUser
import com.exalate.basic.domain.hubobject.v1.BasicHubWorkLog
import com.exalate.replication.services.replication.impersonation.AuditLogService
import org.slf4j.LoggerFactory
import org.slf4j.Logger
import play.libs.Json
import scala.concurrent.Await$;
import scala.concurrent.duration.Duration$;
import java.text.SimpleDateFormat
import java.time.Instant

//Your normal Incoming Sync code
//Add these functions at the end

Worklogs.receive(
	  "B0V4tQJ22LhPnznllWoT2s0N29So5",  // replace the "token" with the previously generated access token
	replica,  
	  replica,
	  issue,
	  httpClient,
	  traces,
	  nodeHelper
)


class Worklogs {

    
 


    static receive(String token, BasicHubIssue replica, BasicHubIssue issue, httpClient, traces, nodeHelper){
        receive(
                token,
                replica,
                issue,
                httpClient,
                traces,
                nodeHelper,
                { BasicHubWorkLog w ->
                    def getUser = { String key ->
                        def localAuthor = nodeHelper.getUser(key)
                        if (localAuthor == null) {
                            localAuthor = new BasicHubUser()
                            localAuthor.key = "557058:c020323a-70e4-4c07-9ccc-3ad89b1c02ec"
                        }
                        localAuthor
                    }
                    w.author = w.author ? getUser(w.author.key) : null
                    w.updateAuthor = w.updateAuthor ? getUser(w.updateAuthor.key) : null
                    w
                }
        )
    }
   
   
    static receive(String token, BasicHubIssue replica, BasicHubIssue issue, httpClient, traces, nodeHelper, Closure<?> onWorklogFn){
        def http = { String method, String path, Map<String, List<String>> queryParams, String body, Map<String, List<String>> headers ->

            def await = { future -> Await$.MODULE$.result(future, Duration$.MODULE$.apply(60, java.util.concurrent.TimeUnit.SECONDS)) }
            def orNull = { scala.Option<?> opt -> opt.isDefined() ? opt.get() : null }
            def pair = { l, r -> scala.Tuple2$.MODULE$.<?, ?>apply(l, r) }
            def none = { scala.Option$.MODULE$.<?> empty() }

            def getGeneralSettings = {
                def classLoader = this.getClassLoader()
                def gsp
                try {
                    gsp = InjectorGetter.getInjector().instanceOf(classLoader.loadClass("com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsRepository"))
                } catch(ClassNotFoundException exception) {
                    gsp = InjectorGetter.getInjector().instanceOf(classLoader.loadClass("com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsPersistence"))
                }
                def gsOpt = await(gsp.get())
                def gs = orNull(gsOpt)
                gs
            }
            final def gs = getGeneralSettings()

            def removeTailingSlash = { String str -> str.trim().replace("/+\$", "") }
            final def tempoRestApiUrl = "https://api.tempo.io/core/3"

            def parseQueryString = { String string ->
                string.split('&').collectEntries{ param ->
                    param.split('=', 2).collect{ URLDecoder.decode(it, 'UTF-8') }
                }
            }

            
            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])
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.mailMap = [
                                recipient: schemeSpecificPartList[0],
                                cc       : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'cc' }.value,
                                bcc      : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'bcc' }.value,
                                subject  : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'subject' }.value,
                                body     : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'body' }.value
                        ]
                    }
                    if (parsedUri.fragment?.contains('?')) { 
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.rawQuery = parsedUri.rawFragment.split('\\?')[1]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.query = parsedUri.fragment.split('\\?')[1]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.rawFragment = parsedUri.rawFragment.split('\\?')[0]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.fragment = parsedUri.fragment.split('\\?')[0]
                    }
                    if (parsedUri.rawQuery) {
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.queryMap = parseQueryString(parsedUri.rawQuery)
                    } else {
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.queryMap = null
                    }

                    //noinspection GrUnresolvedAccess
                    if (parsedUri.queryMap) {
                        //noinspection GrUnresolvedAccess
                        parsedUri.queryMap.keySet().each { key ->
                            def value = parsedUri.queryMap[key]
                            //noinspection GrUnresolvedAccess
                            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
            }

            def unsanitizedUrl = tempoRestApiUrl + path
            def parsedUri = parseUri(unsanitizedUrl)

       
            def embeddedQueryParams = parsedUri.queryMap

            def allQueryParams = embeddedQueryParams instanceof Map ?
                    ({
                        def m = [:] as Map<String, List<String>>;
                        m.putAll(embeddedQueryParams as Map<String, List<String>>)
                        m.putAll(queryParams)
                    })()
                    : (queryParams ?: [:] as 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, 
                        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(queryParams.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)
                }
                response = await(request.execute())


            } catch (Exception e) {
                throw new com.exalate.api.exception.IssueTrackerException("Unable to perform the request $method $path, please contact Exalate Support: ".toString() + e.message, e)
            }
            if (response.status() >= 300) {
                throw new com.exalate.api.exception.IssueTrackerException("Failed to perform the request $method $path ${body ? "with body `$body`".toString() : ""}(status ${response.status()}), please contact Exalate Support: ".toString() + response.body())
            }
            response.body() as String
        }

        def gsp = InjectorGetter.getInjector().instanceOf(AuditLogService.class)

        def js = new groovy.json.JsonSlurper()
        def jo = new groovy.json.JsonOutput()

        def listAdditionalParams = replica.customKeys."tempoWorklogParams" as Map<String, Map<String, Object>>;
        
        replica.workLogs.findAll{it.id == null}.each{ BasicHubWorkLog worklog ->
            def transformedWorklog
            
          
            try {
                transformedWorklog = onWorklogFn(worklog)
            } catch (com.exalate.api.exception.IssueTrackerException ite) {
                throw ite
            } catch (Exception e) {
                throw new com.exalate.api.exception.IssueTrackerException(e)
            }
            if (transformedWorklog instanceof BasicHubWorkLog) {
                worklog = transformedWorklog as BasicHubWorkLog
            } else if (transformedWorklog == null) {
                return
            }

            def auditLogOpt = gsp.createAuditLog(scala.Option$.MODULE$.<String>apply(issue.id as String),
                    WebhookEntityType.WORKLOG_CREATED,
                    worklog.getAuthor().getKey()
            )

            def attributes = ((listAdditionalParams?.get(worklog.remoteId.toString())?.get("attributes") as Map<String, Object>)?.get("values") as List<Map<String, String>>)?.inject([]){
                List<Map<String, String>>result, Map<String, String> attribute ->
                    result.add(
                            [
                                    key: attribute.get("key"),
                                    value: attribute.get("value")
                            ]
                    )
                    result
            } ?: []
            def properties = [
                    issueKey : issue.key,
                    timeSpentSeconds : worklog.getTimeSpent(),
                    billableSeconds: listAdditionalParams?.get(worklog.remoteId.toString())?.get("billableSeconds") ?: worklog.getTimeSpent(),
                    startDate : new java.text.SimpleDateFormat("yyyy-MM-dd").format(worklog.startDate),
                    startTime : new java.text.SimpleDateFormat("hh:mm:ss").format(worklog.startDate) ?: listAdditionalParams?.get(worklog.remoteId.toString())?.get("startTime"),//strDateSplitted[1].split("\\.")[0],
                    description : worklog.getComment(),
                    authorAccountId : worklog.getAuthor().getKey(),
                    remainingEstimateSeconds:  replica.remainingEstimate ?: listAdditionalParams?.get(worklog.remoteId.toString())?.get("remainingEstimateSeconds"),
                    attributes : attributes
            ]
            def jsonTempoPost = jo.toJson(properties)


            def response = js.parseText(http(
                    "POST",
                    "/worklogs",
                    null,
                    jsonTempoPost,
                    [
                            "Authorization":["Bearer ${token}".toString()],
                            "Content-Type":["application/json"],
                    ]
            ))
            println(response)

            gsp.updateAuditLog(scala.Option$.MODULE$.apply(auditLogOpt), issue.id as String, response["jiraWorklogId"] as String, Json.stringify(Json.toJson(response)))


            String localIdStr = response["tempoWorklogId"]
            String remoteIdStr = worklog.remoteId.toString()

            com.exalate.api.domain.twintrace.INonPersistentTrace trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
                    .setLocalId(localIdStr)
                    .setRemoteId(remoteIdStr)
                    .setType(com.exalate.api.domain.twintrace.TraceType.WORKLOG)
                    .setAction(com.exalate.api.domain.twintrace.TraceAction.NONE)
                    .setToSynchronize(true)
            traces.add(trace)
        }



    }

  }

is seamlessly populated within JCB, allowing users to interact with it. However, a critical aspect of worklog synchronization arises when a user adds time to the ticket in JCB. It is imperative that this time entry is reflected in JCA to maintain consistency and facilitate effective project tracking.


Conversely, if a user adds or modifies a worklog in JCA, it should be automatically synchronized with JCB to ensure that all stakeholders have up-to-date information across both instances.


This bidirectional synchronization enables teams working on different Jira Cloud instances to collaborate seamlessly, regardless of their physical location or the specific instance they are using.

By implementing Exalate as the integration solution, organizations can establish a robust and reliable connection between JCA and JCB, facilitating real-time updates of Tempo Worklogs.

Tempo Worklogs


After installing Tempo Worklogs, you will see it getting added under your apps selection. 

Exalate uses Tempo Cloud REST API to get access to the tempo worklogs.


The use of the Tempo Cloud REST API enables Exalate to retrieve worklogs from the source instance and transfer them to the target instance seamlessly. It also allows for the synchronization of worklog updates, ensuring that any modifications made in one instance are accurately reflected in the other.


Generate Access Token


Exalate requires access to Tempo. To grant secure, temporary access to Tempo you need to create a user access token. This access token is based on current permissions of the Jira Cloud user.

Required permissions

The user who generated the access token must have the following permissions:

  • Jira permissions:
    • Create worklogs
    • View all worklogs
    • Log work for others
    • Work on issues
  • Tempo permissions:
    • view team worklogs
    • manage team worklogs

Generate the Access Token under Tempo settings - API integration tab in your Jira Cloud settings.




The Scripts

Jira Cloud 


Outgoing Sync Jira Cloud 


Code Block
languagegroovy
titleOutgoing Sync
linenumberstrue
replica.timeSpent          = issue.timeSpent
replica.originalEstimate   = issue.originalEstimate
replica.remainingEstimate  = issue.remainingEstimate

replica.workLogs = issue.workLogs

//change the tempo worklog integration token
TempoWorkLogSync.send(
    "vykOUXzZvZo5fufy3TX7gAJKrs3ORx-eu")



Incoming Sync Jira Cloud 


Add the imports in the beginning of the code.

Add the functions at the end of the code.

Code Block
languagegroovy
titleIncoming Sync
linenumberstrue
issue.originalEstimate   = replica.originalEstimate
issue.remainingEstimate  = replica.remainingEstimate

if(firstSync){
	//change the project Key
    issue.projectKey  = "MAT"
    issue.typeName    = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"
    issue.summary      = replica.summary
    store(issue)
}

issue.summary      = replica.summary
issue.description  = replica.description
issue.comments     = commentHelper.mergeComments(issue, replica)
issue.attachments  = attachmentHelper.mergeAttachments(issue, replica)
issue.labels       = replica.labels

//change the tempo integration token
TempoWorkLogSync.receive("4E5Gha3JVKspLkjueXh2CIAq1Xs4G-eu",replica,issue,httpClient,traces,nodeHelper)






Please be sure to change the tokens, to the correct TEMPO tokens.



Video


Disclaimer: Please use the above updated scripts instead of the scripts shown in the video. Thank you.

...




Widget Connector
urlhttp://youtube.com/watch?v=r1QW0nnihAY

...