Originally asked by kalyan on 26 May 2022 (original question)
Hi All,
We are trying to synchronize sprints from Jira to Azur Devops.
Below is the requirement:
- We create Sprint under Jira, add issues , work items under sprint.
- The sprint, tasks under have to be synchronized to Azure Devops.
Below are the things we tried:
- Create a scripted connection between Jira, Azure devops.
- Create a trigger with Entity Type set to Sprint.
- Add, Update Outgoing script in Jira to Synchronize Sprint data to Azure Devops.
- Add, Update Incoming script in Azure Devops to synchronize incoming Sprints data from Jira.
But we had errors in Jira, not able to Synch Sprint data.
Added below outgoing Script in Jira:
def boardIds = [“1”] //Boards which sprints will get synced
if(entityType == “sprint” && boardIds.find{it == sprint.originBoardId}){
replica.name = sprint.name
replica.goal = sprint.goal
replica.state = sprint.state
replica.startDate = sprint.startDate
replica.endDate = sprint.endDate
replica.originBoardId = sprint.originBoardId
}
Added below Incoming Script in Azure devops:
if(entityType == “sprint”){
//Executed when receiving a sprint sync from the remote side
def sprintMap = [“1”:“2”] //[remoteBoardId: localBoardId]
[sprint.name](http://sprint.name) \= [replica.name](http://replica.name)
sprint.goal \= replica.goal
sprint.state \= replica.state
sprint.startDate \= replica.startDate
sprint.endDate \= replica.endDate
def localBoardId \= sprintMap\[replica.originBoardId]
if(localBoardId \=\= null){
throw new com.exalate.api.exception.IssueTrackerException("No board mapping for remote board id "\+replica.originBoardId)
}
sprint.originBoardId \= localBoardId //Set the board ID where the sprint will be created
}
Please let me know your valuable suggestions.
Thank you.
Comments:
Serhiy Onyshchenko commented on 28 May 2022
Hello, kalyan
documenting something for my future self, concerning your question:
JIRA OUT
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 = []
replica.customFields."Sprint" = issue.customFields."Sprint"
/*
Custom Fields
replica.customFields."CF Name" = issue.customFields."CF Name"
*/
ADO IN
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)
def getCurrentSprint = { -> replica."Sprint".find {!it.state.equalsIgnoreCase("CLOSED")} }
if (replica."Sprint" != null && !replica."Sprint".empty && getCurrentSprint() != null) {
def project = connection.trackerSettings.fieldValues."project"
def area = workItem.areaPath ?: workItem.project?.key ?: project
def sprint = getCurrentSprint()
def iteration = sprint.name
def iterationPath = area + "\\" + iteration
if (iterationPath != workItem.iterationPath) {
//debug.error("fV.p=${connection.fieldValues."project"}")
//find existing iterations
//debug.error("methods=${httpClient.getClass().declaredMethods}")
def existingIterations = httpClient.get("/${project}/_apis/wit/classificationnodes/Iterations?\$depth=1&api-version=5.0", false)?.children
/*httpClient.http (
"GET",
"/${project}/_apis/wit/classificationnodes".toString(),
null,
["\$depth":["1"],"api-version":["5.0"]],
[:]
) { response ->
if (response.code >= 300) { debug.error("GET /${project}/_apis/wit/classificationnodes/Iterations?\$depth=1&api-version=5.0 failed: ${response.body}") }
def json = response.body
if (json.children == null) {
debug.error("GET /${project}/_apis/wit/classificationnodes/Iterations?\$depth=1&api-version=5.0 didn't find iteration paths: ${response.body}")
}
json.children
}*/
//debug.error("existingIterations=${existingIterations}")
if (!existingIterations.name.any {it.equalsIgnoreCase(sprint.name)}) {
//if we need to create iterations
def await = { f -> scala.concurrent.Await$.MODULE$.result(f, scala.concurrent.duration.Duration.apply(1, java.util.concurrent.TimeUnit.MINUTES)) }
def creds = await(httpClient.azureClient.getCredentials())
def token = creds.accessToken()
def baseUrl = creds.issueTrackerUrl()
def dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
def createIterationBody = [name:sprint.name]
def attributes = null
if(sprint.startDate) {
def sd = dateFormat.format(sprint.startDate)
attributes = ["startDate":sd]
}
if(sprint.endDate) {
def ed = dateFormat.format(sprint.endDate)
attributes = attributes ?: [:]
attributes."finishDate" = ed
}
if(attributes != null) {
createIterationBody."attributes" = attributes
}
def createIterationBodyStr = groovy.json.JsonOutput.toJson(createIterationBody)
def result = await(httpClient.azureClient.ws
.url(baseUrl+"/${project}/_apis/wit/classificationnodes/Iterations?api-version=5.0")
.withAuth(token, token, play.api.libs.ws.WSAuthScheme$BASIC$.MODULE$)
.withBody(play.api.libs.json.Json.parse(createIterationBodyStr), play.api.libs.ws.JsonBodyWritables$.MODULE$.writeableOf_JsValue)
//.addHttpHeaders([scala.Tuple2.apply("Content-Type", "applicatio")])
.withMethod("POST")
.execute())
debug.error("result=${result} status=${result.status}")
//httpClient.post("/${project}/_apis/wit/classificationnodes/Iterations?api-version=5.0".toString(), createIterationBodyStr)
/*httpClient.http(
"POST",
"/${project}/_apis/wit/classificationnodes/Iterations".toString(),
createIterationBodyStr,
["api-version":["5.0"]],
[:]
) { response ->
if (response.code >= 300) { debug.error("POST /${project}/_apis/wit/classificationnodes/Iterations?api-version=5.0 with body: ${createIterationBodyStr} failed: ${response.body}") }
def json = response.body
if (json.id == null) {
debug.error("POST /${project}/_apis/wit/classificationnodes/Iterations?api-version=5.0 with body: ${createIterationBodyStr} didn't create a sprint: ${response.body}")
}
json
}*/
}
workItem.iterationPath = iterationPath
}
//debug.error("it=${workItem.iterationPath}")
}
/*
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)
*/
Regards, Serhiy
kalyan commented on 02 June 2022
Hi Serhiy,
Thank you.
I am facing below issue in Jira after adding above script from your reply.
Created below trigger in Jira:
Jira Out:
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 =
replica.customFields.“Sprint” = issue.customFields.“Sprint”
/*
Custom Fields
replica.customFields.“CF Name” = issue.customFields.“CF Name”
*/
Below is the error:
Error Stack Trace
com.exalate.api.exception.script.ScriptException: No such property: issue for class: Script24 at com.exalate.error.services.ScriptExceptionCategoryService.categorizeProcessorAndIssueTrackerExceptionsIntoScriptExceptions(ScriptExceptionCategoryService.scala:40) at com.exalate.processor.ExalateProcessor.executeProcessor(ExalateProcessor.java:57) at com.exalate.replication.services.processor.CreateReplicaProcessor.$anonfun$executeScriptRules$2(CreateReplicaProcessor.scala:119) at scala.util.Try$.apply(Try.scala:213) at com.exalate.replication.services.processor.CreateReplicaProcessor.executeScriptRules(CreateReplicaProcessor.scala:117) at com.exalate.replication.services.processor.CreateReplicaProcessor.$anonfun$createHubReplica$2(CreateReplicaProcessor.scala:69) at scala.concurrent.Future.$anonfun$flatMap$1(Future.scala:307) at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:41) at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64) at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:56) at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:93) at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23) at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:85) at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:93) at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:48) at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(ForkJoinExecutorConfigurator.scala:48) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183) Caused by: javax.script.ScriptException: javax.script.ScriptException: groovy.lang.MissingPropertyException: No such property: issue for class: Script24 at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:158) at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264) at com.exalate.processor.ExalateProcessor.execute(ExalateProcessor.java:98) at com.exalate.processor.ExalateProcessor.executeProcessor(ExalateProcessor.java:55) … 19 more Caused by: javax.script.ScriptException: groovy.lang.MissingPropertyException: No such property: issue for class: Script24 at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:320) at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:155) … 22 more Caused by: groovy.lang.MissingPropertyException: No such property: issue for class: Script24 at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:65) at org.codehaus.groovy.runtime.callsite.PogoGetPropertySite.getProperty(PogoGetPropertySite.java:51) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callGroovyObjectGetProperty(AbstractCallSite.java:309) at Script24.run(Script24.groovy:1) at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:317) … 23 more
Are we missing any thing?
Please let us know.
Serhiy Onyshchenko commented on 16 March 2023
Hello, kalyan ,
Please, remove the sprint trigger and unexalate the sprint you’d attempted to sync - the sprint is going to be synced as part of the issue sync.
Syed Majid Hassan , I’m sure my explanation is cryptic, please help me.
Regards, Serhiy