JIRA Outgoing.groovy
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 = [] /* Custom Fields replica.customFields."CF Name" = issue.customFields."CF Name" */
JIRA Incoming.groovy
import com.atlassian.jira.issue.link.IssueLinkManager import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.security.JiraAuthenticationContext import com.atlassian.jira.issue.link.IssueLinkTypeManager import com.atlassian.jira.issue.link.IssueLinkType import org.slf4j.Logger class LogIn { static logIn(u) { def authCtx = com.atlassian.jira.component.ComponentAccessor.getJiraAuthenticationContext() try { //Jira 7 authCtx.setLoggedInUser(u) } catch (Exception ignore) { // Jira 6 //noinspection GroovyAssignabilityCheck authCtx.setLoggedInUser(u.getDirectoryUser()) } } static <R> R tryLogInFinallyLogOut(Closure<R> fn) { def authCtx = com.atlassian.jira.component.ComponentAccessor.getJiraAuthenticationContext() def proxyAppUser = getProxyUser() def loggedInUser = authCtx.getLoggedInUser() try { logIn(proxyAppUser) fn() } finally { logIn(loggedInUser) } } static getProxyUser() { def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) nserv.proxyUser } } class CreateIssue { static def log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Epic") private static def doCreate = { com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, com.exalate.api.domain.request.ISyncRequest syncRequest, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issueBeforeScript, com.exalate.api.domain.INonPersistentReplica remoteReplica, List<com.exalate.api.domain.twintrace.INonPersistentTrace> traces, List<com.exalate.api.domain.IBlobMetadata> blobMetadataList, Logger log -> def firstSync = com.exalate.processor.jira.JiraCreateIssueProcessor.createProcessorContext.get() == true def issueLevelError = { String msg -> new com.exalate.api.exception.IssueTrackerException(msg) } def issueLevelError2 = { String msg, Throwable c -> new com.exalate.api.exception.IssueTrackerException(msg, c) } def toExIssueKey = { com.atlassian.jira.issue.MutableIssue i -> new com.exalate.basic.domain.BasicIssueKey(i.id, i.key) } final def authCtxInternal = com.atlassian.jira.component.ComponentAccessor.getJiraAuthenticationContext() final def imInternal = com.atlassian.jira.component.ComponentAccessor.issueManager final def umInternal = com.atlassian.jira.component.ComponentAccessor.userManager final def nservInternal = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) final def hohfInternal2 = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.hubobject.IHubObjectHelperFactory.class) //noinspection GroovyAssignabilityCheck final def hohInternal2 = hohfInternal2.get(remoteReplica.payload.version) if (issue.id != null) { def existingIssue = imInternal.getIssueObject(issue.id as Long) if (existingIssue != null) { return [existingIssue, toExIssueKey(existingIssue)] } } def proxyAppUserInternal = nservInternal.getProxyUser() def loggedInUser = authCtxInternal.getLoggedInUser() log.debug("Logged user is " + loggedInUser) def reporterAppUser = null if (issue.reporter != null) { reporterAppUser = umInternal.getUserByKey(issue.reporter?.key) } reporterAppUser = reporterAppUser ?: proxyAppUserInternal issue.project = issue.project ?: ({ nodeHelper.getProject(issue.projectKey) })() issue.type = issue.type ?: ({ nodeHelper.getIssueType(issue.typeName) })() def jIssueInternal = null try { LogIn.logIn(reporterAppUser) if (issue.id != null) { def existingIssue = imInternal.getIssueObject(issue.id as Long) if (existingIssue != null) { issue.id = existingIssue.id issue.key = existingIssue.key return [existingIssue, toExIssueKey(existingIssue)] } } def cir try{ cir = hohInternal2.createNodeIssueWith(issue, hohInternal2.createHubIssueTemplate(), null, [:], blobMetadataList, syncRequest) } catch (MissingMethodException e){ cir = hohInternal2.createNodeIssueWith(issue, hohInternal2.createHubIssueTemplate(), null, [:], blobMetadataList, syncRequest.getConnection()) } def createdIssueKey = cir.getIssueKey(); jIssueInternal = imInternal.getIssueObject(createdIssueKey.id) if (issue.id != null) { def oldIssueKey = jIssueInternal.key def oldIssueId = jIssueInternal.id try { jIssueInternal.key = issue.key jIssueInternal.store() } catch (Exception e) { log.error("""Failed to sync issue key: ${e.message}. Please contact Exalate Support. Deleting issue `$oldIssueKey` ($oldIssueId)""".toString(), e) imInternal.deleteIssue(proxyAppUserInternal, jIssueInternal as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } } issue.id = jIssueInternal.id issue.key = jIssueInternal.key return [jIssueInternal, toExIssueKey(jIssueInternal)] } catch (com.exalate.api.exception.IssueTrackerException ite) { if (firstSync && jIssueInternal != null) { imInternal.deleteIssue(proxyAppUserInternal, jIssueInternal as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } throw ite } catch (Exception e) { if (firstSync && jIssueInternal != null) { imInternal.deleteIssue(proxyAppUserInternal, jIssueInternal as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } throw issueLevelError2("""Failed to create issue: ${ e.message }. Please review the script or contact Exalate Support""".toString(), e) } finally { LogIn.logIn(loggedInUser) } } /** * @param whenIssueCreatedFn - a callback closure executed after the issue has been created * */ static com.exalate.basic.domain.BasicIssueKey create( com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, com.exalate.api.domain.request.ISyncRequest syncRequest, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issueBeforeScript, com.exalate.api.domain.INonPersistentReplica remoteReplica, List<com.exalate.api.domain.twintrace.INonPersistentTrace> traces, List<com.exalate.api.domain.IBlobMetadata> blobMetadataList, Closure<?> whenIssueCreatedFn) { def firstSync = com.exalate.processor.jira.JiraCreateIssueProcessor.createProcessorContext.get() == true def (_jIssue, _exIssueKey) = doCreate(replica, issue, syncRequest, nodeHelper, issueBeforeScript, remoteReplica, traces, blobMetadataList, log) com.atlassian.jira.issue.MutableIssue jIssue = _jIssue as com.atlassian.jira.issue.MutableIssue com.exalate.basic.domain.BasicIssueKey exIssueKey = _exIssueKey as com.exalate.basic.domain.BasicIssueKey try { whenIssueCreatedFn() UpdateIssue.update(replica, issue, syncRequest, nodeHelper, issueBeforeScript, traces, blobMetadataList, jIssue, exIssueKey) } catch (Exception e3) { final def imInternal = com.atlassian.jira.component.ComponentAccessor.issueManager final def nservInternal2 = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def proxyAppUserInternal = nservInternal2.getProxyUser() if (firstSync && _jIssue != null) { imInternal.deleteIssue(proxyAppUserInternal, _jIssue as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } throw e3 } return exIssueKey } } class UpdateIssue { private static def log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Epic") private static def doUpdate = { com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, com.exalate.api.domain.request.ISyncRequest syncRequest, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issueBeforeScript, List<com.exalate.api.domain.twintrace.INonPersistentTrace> traces, List<com.exalate.api.domain.IBlobMetadata> blobMetadataList, com.atlassian.jira.issue.MutableIssue jIssue, com.exalate.basic.domain.BasicIssueKey exIssueKey -> try { final def hohfInternal2 = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.hubobject.IHubObjectHelperFactory.class) //noinspection GroovyAssignabilityCheck final def hohInternal2 = hohfInternal2.get("1.2.0") final def nservInternal2 = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def proxyAppUserInternal2 = nservInternal2.getProxyUser() log.info("performing the update for the issue `" + jIssue.key + "` for remote issue `" + replica.key + "`") //finally create all def fakeTraces2 = com.exalate.util.TraceUtils.indexFakeTraces(traces) def preparedIssue2 = hohInternal2.prepareLocalHubIssueForApplication(issueBeforeScript, issue, fakeTraces2) //@Nonnull IIssueKey issueKey, @Nonnull IHubIssueReplica hubIssueAfterScripts, @Nullable String proxyUser, @Nonnull IHubIssueReplica hubIssueBeforeScripts, @Nonnull Map<TraceType, List<ITrace>> traces, @Nonnull List<IBlobMetadata> blobMetadataList, IRelation relation def resultTraces2 try{ resultTraces2 = hohInternal2.updateNodeIssueWith(exIssueKey, preparedIssue2, proxyAppUserInternal2.key, issueBeforeScript, fakeTraces2, blobMetadataList, syncRequest) } catch (MissingMethodException e){ resultTraces2 = hohInternal2.updateNodeIssueWith(exIssueKey, preparedIssue2, proxyAppUserInternal2.key, issueBeforeScript, fakeTraces2, blobMetadataList, syncRequest.getConnection()) } traces.clear() traces.addAll(resultTraces2 ?: []) new Result(issue, traces) } catch (com.exalate.api.exception.IssueTrackerException ite2) { throw ite2 } catch (Exception e2) { throw new com.exalate.api.exception.IssueTrackerException(e2.message, e2) } } static Result update(com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, com.exalate.api.domain.request.ISyncRequest syncRequest, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issueBeforeScript, List<com.exalate.api.domain.twintrace.INonPersistentTrace> traces, List<com.exalate.api.domain.IBlobMetadata> blobMetadataList, com.atlassian.jira.issue.MutableIssue jIssue, com.exalate.basic.domain.BasicIssueKey exIssueKey) { doUpdate(replica, issue, syncRequest, nodeHelper, issueBeforeScript, traces, blobMetadataList, jIssue, exIssueKey) } static class Result { com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue List<com.exalate.api.domain.twintrace.INonPersistentTrace> traces Result(com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, java.util.List<com.exalate.api.domain.twintrace.INonPersistentTrace> traces) { this.issue = issue this.traces = traces } } } int createIssueLink(){ if (replica.parentId || replica."relation"){ def parentLinkExists = false if (replica.parentId) flag = true def localParentKey = nodeHelper.getLocalIssueKeyFromRemoteId(replica.parentId ?: replica?."relationid" as Long, "issue")?.key if (localParentKey==null) return 1 final String sourceIssueKey = localParentKey final String destinationIssueKey = issue.key def linkTypeMap = [ "Parent" : "Relates", "Duplicate" : "Duplicate" ] String issueLinkName if (!parentLinkExists) issueLinkName = linkTypeMap[replica."relation"] else issueLinkName = "Blocks" final Long sequence = 1L def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser def issueLinkTypeManager = ComponentAccessor.getComponent(IssueLinkTypeManager) def issueManager = ComponentAccessor.issueManager def sourceIssue = issueManager.getIssueByCurrentKey(sourceIssueKey) def destinationIssue = issueManager.getIssueByCurrentKey(destinationIssueKey) def availableIssueLinkTypes = issueLinkTypeManager.issueLinkTypes int i,f=999 for (i=0; i<availableIssueLinkTypes.size() ; i++){ if(issueLinkName.equals(availableIssueLinkTypes[i].name)){ f = i break } } ComponentAccessor.issueLinkManager.createIssueLink(sourceIssue.id, destinationIssue.id, availableIssueLinkTypes[f].id, sequence, loggedInUser) } return 1 } if(firstSync){ issue.projectKey = "PM00" def issueMap = [ "Epic" :"Story", "Feature" :"Task", "Task" :"Bug" ] issue.typeName = issueMap[replica.type?.name] CreateIssue.create( replica, issue, syncRequest, nodeHelper, issueBeforeScript, remoteReplica, traces, blobMetadataList) { if (replica.parentId || replica."relation") createIssueLink() } } issue.summary = replica.summary issue.description = replica.description issue.comments = commentHelper.mergeComments(issue, replica) issue.attachments = attachmentHelper.mergeAttachments(issue, replica) issue.labels = replica.labels //if (replica.parentId || replica."relation") // createIssueLink() /* User Synchronization (Assignee/Reporter) Set a Reporter/Assignee from the source side, if the user can't be found set a default user You can use this approach for custom fields of type User def defaultUser = nodeHelper.getUserByEmail("default@idalko.com") issue.reporter = nodeHelper.getUserByEmail(replica.reporter?.email) ?: defaultUser issue.assignee = nodeHelper.getUserByEmail(replica.assignee?.email) ?: defaultUser */ /* Comment Synchronization Sync comments with the original author if the user exists in the local instance Remove original Comments sync line if you are using this approach issue.comments = commentHelper.mergeComments(issue, replica){ it.executor = nodeHelper.getUserByEmail(it.author?.email) } */ /* Status Synchronization Sync status according to the mapping [remote issue status: local issue status] If statuses are the same on both sides don't include them in the mapping def statusMapping = ["Open":"New", "To Do":"Backlog"] def remoteStatusName = replica.status.name issue.setStatus(statusMapping[remoteStatusName] ?: remoteStatusName) */ /* Custom Fields This line will sync Text, Option(s), Number, Date, Organization, and Labels CFs For other types of CF check documentation issue.customFields."CF Name".value = replica.customFields."CF Name".value */
ADO Outgoing.groovy
replica.key = workItem.key replica.assignee = workItem.assignee replica.summary = workItem.summary replica.description = nodeHelper.stripHtml(workItem.description) replica.type = workItem.type replica.status = workItem.status replica.labels = workItem.labels replica.priority = workItem.priority replica.comments = nodeHelper.stripHtmlFromComments(workItem.comments) replica.attachments = workItem.attachments replica.project = workItem.project replica.areaPath = workItem.areaPath replica.iterationPath = workItem.iterationPath replica.parentId = workItem.parentId def res = httpClient.get("/_apis/wit/workitems/${workItem.key}?\$expand=relations&api-version=6.0",false) if (res.relations != null){ replica."relation" = res.relations[0].attributes.name replica."relationid" = (res.relations[0].url).tokenize('/')[7] }
ADO Incoming.groovy
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) /* 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) */