The main requirement for this use case is to maintain the multi-level issue hierarchy while Exalating issues from Azure DevOps to Jira Server/DC.
The relationship on Azure DevOps is Epic → Feature → Task, and this should be mirrored as Story → Task → Bug on Jira Server. The following depicts what we are trying to achieve here:
In the Outgoing script of Azure DevOps, we need to ensure that the relationships between the different workItems is sent across to Jira. The following code snippet achieves this:
Incoming sync ADO
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]
}
The rest of the magic happens in the Jira Incoming script. The full code is as follows:
Incoming Sync Jira On-Premise
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
Please review the following video to see the use case in action:
