← Назад к статьям

Интеграции Jira с внешними системами через ScriptRunner

Интеграция Jira с внешними системами — частая задача в корпоративных средах. Нужно синхронизировать данные с CRM, отправлять уведомления в Slack, интегрироваться с CI/CD, обмениваться данными с другими системами. ScriptRunner позволяет создавать интеграции через REST API, вебхуки и другие механизмы без разработки плагинов. В этой статье разберу практические примеры интеграций.

Типы интеграций

Основные типы интеграций через ScriptRunner:

  • Исходящие запросы — Jira отправляет данные во внешние системы
  • Входящие запросы — внешние системы отправляют данные в Jira через REST API
  • Двусторонняя синхронизация — обмен данными в обе стороны
  • Вебхуки — отправка уведомлений о событиях в Jira

Отправка данных во внешние системы

Пример отправки данных о задаче в внешнюю систему при её создании:

import groovy.json.JsonBuilder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.URI

def event = event
def issue = event.issue
def eventTypeId = event.eventTypeId

if (eventTypeId != com.atlassian.jira.event.type.EventType.ISSUE_CREATED_ID) {
    return
}

def externalApiUrl = "https://external-api.company.com/issues"

def payload = [
    jiraKey: issue.key,
    summary: issue.summary,
    description: issue.description,
    status: issue.status.name,
    assignee: issue.assignee?.username,
    reporter: issue.reporter?.username,
    created: issue.created.toString()
]

def json = new JsonBuilder(payload).toPrettyString()

def client = HttpClient.newHttpClient()
def request = HttpRequest.newBuilder()
    .uri(URI.create(externalApiUrl))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer YOUR_API_TOKEN")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build()

try {
    def response = client.send(request, HttpResponse.BodyHandlers.ofString())
    if (response.statusCode() == 200 || response.statusCode() == 201) {
        log.warn("Successfully sent issue ${issue.key} to external system")
    } else {
        log.error("Failed to send issue ${issue.key}: ${response.statusCode()}")
    }
} catch (Exception e) {
    log.error("Error sending issue ${issue.key}: ${e.message}")
}

Интеграция с Slack

Отправка уведомлений в Slack при изменении статуса задачи:

import groovy.json.JsonBuilder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.URI

def event = event
def issue = event.issue

def slackWebhookUrl = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

def message = [
    text: "Задача ${issue.key} изменила статус",
    blocks: [
        [
            type: "section",
            text: [
                type: "mrkdwn",
                text: "*<${getBaseUrl()}/browse/${issue.key}|${issue.key}>* - ${issue.summary}\nСтатус: *${issue.status.name}*"
            ]
        ]
    ]
]

def json = new JsonBuilder(message).toPrettyString()

def client = HttpClient.newHttpClient()
def request = HttpRequest.newBuilder()
    .uri(URI.create(slackWebhookUrl))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build()

try {
    client.send(request, HttpResponse.BodyHandlers.ofString())
} catch (Exception e) {
    log.error("Failed to send Slack notification: ${e.message}")
}

Интеграция с CI/CD

Обновление статуса задачи в Jira при деплое из CI/CD системы:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.MutableIssue
import groovy.json.JsonSlurper

// Данные приходят через REST API эндпоинт
def requestBody = httpMethod.getRequestBodyAsString()
def json = new JsonSlurper().parseText(requestBody)

def issueKey = json.issueKey
def deployStatus = json.status // "success" или "failure"

def issueManager = ComponentAccessor.getIssueManager()
def issue = issueManager.getIssueObject(issueKey)

if (!issue) {
    return Response.status(404).entity([error: "Issue not found"]).build()
}

def mutableIssue = issueManager.getIssueObject(issue.id) as MutableIssue

// Обновляем кастомное поле "Deploy Status"
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def deployStatusField = customFieldManager.getCustomFieldObjectByName("Deploy Status")
mutableIssue.setCustomFieldValue(deployStatusField, deployStatus)

def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
issueManager.updateIssue(currentUser, mutableIssue, 
    com.atlassian.jira.event.type.EventDispatchOption.ISSUE_UPDATED, false)

return Response.ok([success: true]).build()

Синхронизация с базой данных

Пример синхронизации данных задачи с внешней базой данных:

import groovy.sql.Sql
import com.atlassian.jira.component.ComponentAccessor

def issue = event.issue

// Подключение к базе данных (настройте под вашу БД)
def dbUrl = "jdbc:postgresql://db.company.com:5432/external_db"
def dbUser = "jira_sync"
def dbPassword = "password"

def sql = Sql.newInstance(dbUrl, dbUser, dbPassword, "org.postgresql.Driver")

try {
    // Проверяем, существует ли запись
    def existing = sql.firstRow(
        "SELECT id FROM jira_sync WHERE jira_key = ?", 
        [issue.key]
    )
    
    if (existing) {
        // Обновляем существующую запись
        sql.executeUpdate(
            """UPDATE jira_sync 
               SET summary = ?, status = ?, updated_at = NOW() 
               WHERE jira_key = ?""",
            [issue.summary, issue.status.name, issue.key]
        )
    } else {
        // Создаём новую запись
        sql.executeInsert(
            """INSERT INTO jira_sync (jira_key, summary, status, created_at, updated_at) 
               VALUES (?, ?, ?, NOW(), NOW())""",
            [issue.key, issue.summary, issue.status.name]
        )
    }
} catch (Exception e) {
    log.error("Failed to sync to database: ${e.message}")
} finally {
    sql.close()
}

Получение данных из внешних систем

Пример получения данных из внешнего API и обновления задачи:

import groovy.json.JsonSlurper
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.URI

def issue = event.issue

def externalApiUrl = "https://external-api.company.com/data/${issue.key}"

def client = HttpClient.newHttpClient()
def request = HttpRequest.newBuilder()
    .uri(URI.create(externalApiUrl))
    .header("Authorization", "Bearer YOUR_API_TOKEN")
    .GET()
    .build()

try {
    def response = client.send(request, HttpResponse.BodyHandlers.ofString())
    
    if (response.statusCode() == 200) {
        def json = new JsonSlurper().parseText(response.body())
        
        // Обновляем кастомное поле на основе данных из внешней системы
        def customFieldManager = ComponentAccessor.getCustomFieldManager()
        def externalDataField = customFieldManager.getCustomFieldObjectByName("External Data")
        
        def mutableIssue = ComponentAccessor.getIssueManager().getIssueObject(issue.id) as MutableIssue
        mutableIssue.setCustomFieldValue(externalDataField, json.value)
        
        def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
        ComponentAccessor.getIssueManager().updateIssue(currentUser, mutableIssue, 
            com.atlassian.jira.event.type.EventDispatchOption.DO_NOT_DISPATCH, false)
    }
} catch (Exception e) {
    log.error("Failed to fetch external data: ${e.message}")
}

Обработка ошибок и повторные попытки

При интеграциях важно обрабатывать ошибки и реализовывать механизм повторных попыток:

def maxRetries = 3
def retryDelay = 1000 // миллисекунды

def attemptRequest(url, payload) {
    for (int i = 0; i < maxRetries; i++) {
        try {
            def response = sendRequest(url, payload)
            if (response.statusCode() >= 200 && response.statusCode() < 300) {
                return response
            }
        } catch (Exception e) {
            log.warn("Attempt ${i + 1} failed: ${e.message}")
            if (i < maxRetries - 1) {
                Thread.sleep(retryDelay * (i + 1)) // Экспоненциальная задержка
            }
        }
    }
    throw new Exception("Failed after ${maxRetries} attempts")
}

Выводы

ScriptRunner позволяет создавать мощные интеграции Jira с внешними системами без разработки плагинов. Используйте REST API для обмена данными, обрабатывайте ошибки, реализуйте повторные попытки для надёжности.

Всегда проверяйте аутентификацию при работе с внешними API, логируйте все операции для отладки, тестируйте интеграции на тестовых средах перед применением на production.

Если нужна помощь с созданием интеграций — свяжитесь со мной.