How do I sync workItem links between 2 different Azure DevOps instances?

Hi Everyone,

You can refer to the below solution.

Source ADO Outgoing

def res = httpClient.get("/_apis/wit/workitems/${workItem.key}?\$expand=relations&api-version=6.0",false)
if (res.relations != null)
    replica.relations = res.relations

Target ADO Incoming


def await = { f -> scala.concurrent.Await$.MODULE$.result(f, scala.concurrent.duration.Duration.apply(1, java.util.concurrent.TimeUnit.MINUTES)) }
def credsFuture = httpClient.azureClient.getPATCredentials()
def creds = await(credsFuture)
def token = creds.patAccessToken()
def baseUrl = creds.issueTrackerUrl()
def project = workItem.projectKey

def relations = replica.relations

if (relations) {
    def createRelationsBody = []

    def workItemUrlPattern = ~/https:\/\/dev\.azure\.com\/[^\/]+\/[^\/]+\/_apis\/wit\/workItems\/(\d+)/

    relations.each { relation ->
        def matcher = relation.url =~ workItemUrlPattern
        if (matcher) {
            def remoteWorkItemId = matcher[0][1].toLong()
            def localLinkedItem = syncHelper.getLocalIssueKeyFromRemoteId(remoteWorkItemId)

            if (localLinkedItem?.id) {
                def localLinkedUrl = "${baseUrl}/_apis/wit/workItems/${localLinkedItem.id}"

                // Build JSON patch operation
                createRelationsBody << [
                    op: "add",
                    path: "/relations/-",
                    value: [
                        rel: relation.rel,
                        url: localLinkedUrl,
                        attributes: [comment: "Synced via Exalate"]
                    ]
                ]
            } else {
                log.warn("Linked item with remote ID ${remoteWorkItemId} not found locally — skipping.")
            }
        } else {
            log.debug("No valid work item ID found in URL ${relation.url}")
        }
    }

    if (!createRelationsBody.isEmpty()) {
        def jsonBody = groovy.json.JsonOutput.toJson(createRelationsBody)
        def converter = scala.collection.JavaConverters
        def headers = [new scala.Tuple2("Content-Type", "application/json-patch+json")]
        def scalaSeq = converter.asScalaIteratorConverter(headers.iterator()).asScala().toSeq()

        def response = await(
            httpClient.azureClient.ws
                .url("${baseUrl}/_apis/wit/workitems/${workItem.id}?api-version=6.0")
                .addHttpHeaders(scalaSeq)
                .withAuth(token, token, play.api.libs.ws.WSAuthScheme$BASIC$.MODULE$)
                .withBody(play.api.libs.json.Json.parse(jsonBody), play.api.libs.ws.JsonBodyWritables$.MODULE$.writeableOf_JsValue)
                .withMethod("PATCH")
                .execute()
        )

        if (response.status() >= 200 && response.status() < 300) {
            log.info("✅ Relations updated successfully for ${workItem.id}")
        } else {
            log.error("❌ Failed to update relations for ${workItem.id} — HTTP ${response.status()}: ${response.body()}")
        }
    } else {
        log.info("No new relations to create for ${workItem.id}")
    }
}

Thanks, Dhiren