1
0
-1

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

  1. Serhiy Onyshchenko

    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

  2. kalyan

    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.


  3. Serhiy Onyshchenko

    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

CommentAdd your comment...

1 answer

  1.  
    1
    0
    -1

    Hello, kalyan 
    Please, unexalate all the sprints - the approach I'm suggesting is syncing sprints as part of the issue sync.

    Note, that since DevOps sprints don't have status, updates to sprints seem less important to be synced.
    Regards, Serhiy.

      CommentAdd your comment...