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

Обработка событий Jira через ScriptRunner

Слушатели событий (Event Listeners) — это мощный механизм автоматизации в Jira. Они позволяют реагировать на события в системе: создание задач, изменение статусов, обновление полей, комментирование. ScriptRunner позволяет создавать слушатели событий через Groovy-скрипты без разработки плагинов. В этой статье разберу практические примеры использования слушателей событий.

Типы событий в Jira

Jira генерирует множество событий при различных действиях:

  • Issue Created — создание задачи
  • Issue Updated — обновление задачи
  • Issue Assigned — назначение исполнителя
  • Issue Resolved — решение задачи
  • Issue Closed — закрытие задачи
  • Comment Added — добавление комментария
  • Worklog Added — добавление списанного времени

Слушатели событий получают уведомления о этих событиях и могут выполнять любые действия в ответ.

Создание простого слушателя событий

Создайте слушатель через: Administration → ScriptRunner → Listeners → Script Listeners → Create listener.

Пример слушателя, который логирует создание задач:

import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.issue.Issue

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

if (eventTypeId == com.atlassian.jira.event.type.EventType.ISSUE_CREATED_ID) {
    log.warn("Issue created: ${issue.key} - ${issue.summary}")
    log.warn("Reporter: ${issue.reporter?.displayName}")
    log.warn("Project: ${issue.projectObject.name}")
}

Обработка обновления задач

Слушатель для обработки обновления задач — один из самых полезных. Можно отслеживать изменения полей:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.issue.history.ChangeLogManager

def event = event
def issue = event.issue
def changeLogManager = ComponentAccessor.getChangeLogManager()
def changelog = changeLogManager.getChangeLogForIssue(event)

if (!changelog) {
    return
}

changelog.related("ChildChangeItem").each { item ->
    def field = item.field
    def oldValue = item.fromString
    def newValue = item.toString
    
    log.warn("Field changed: ${field}")
    log.warn("Old value: ${oldValue}")
    log.warn("New value: ${newValue}")
    
    // Можно выполнить действия на основе изменений
    if (field == "assignee" && newValue) {
        notifyNewAssignee(issue, newValue)
    }
}

Автоматическое назначение при создании

Автоматически назначаем задачу на основе типа или компонента:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.issue.MutableIssue

def event = event
def eventTypeId = event.eventTypeId

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

def issue = event.issue as MutableIssue
def issueManager = ComponentAccessor.getIssueManager()
def userManager = ComponentAccessor.getUserManager()

// Назначаем на основе типа задачи
def assignee = null
if (issue.issueType.name == "Bug") {
    assignee = userManager.getUserByKey("qa.lead")
} else if (issue.issueType.name == "Story") {
    assignee = userManager.getUserByKey("product.owner")
}

if (assignee) {
    issue.setAssignee(assignee)
    def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
    issueManager.updateIssue(currentUser, issue, com.atlassian.jira.event.type.EventDispatchOption.DO_NOT_DISPATCH, false)
}

Автоматическое создание подзадач

При создании задачи определённого типа автоматически создаём подзадачи:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.issue.IssueFactory
import com.atlassian.jira.issue.IssueManager

def event = event
def eventTypeId = event.eventTypeId

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

def issue = event.issue

// Создаём подзадачи только для определённого типа
if (issue.issueType.name != "Epic") {
    return
}

def issueManager = ComponentAccessor.getIssueManager()
def issueFactory = ComponentAccessor.getIssueFactory()
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

def subTasks = [
    [name: "Разработка", assignee: "dev.lead"],
    [name: "Тестирование", assignee: "qa.lead"],
    [name: "Документация", assignee: "tech.writer"]
]

subTasks.each { task ->
    def subTask = issueFactory.getIssue()
    subTask.setProjectObject(issue.projectObject)
    subTask.setIssueTypeId("5") // ID типа подзадачи
    subTask.setParentId(issue.id)
    subTask.setSummary("${task.name} для ${issue.summary}")
    subTask.setReporter(currentUser)
    
    if (task.assignee) {
        def assignee = ComponentAccessor.getUserManager().getUserByKey(task.assignee)
        subTask.setAssignee(assignee)
    }
    
    issueManager.createIssueObject(currentUser, subTask)
}

Уведомления во внешние системы

Отправляем уведомления в 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 eventTypeId = event.eventTypeId

// Отслеживаем только изменения статуса
if (eventTypeId != com.atlassian.jira.event.type.EventType.ISSUE_UPDATED_ID) {
    return
}

def changeLogManager = ComponentAccessor.getChangeLogManager()
def changelog = changeLogManager.getChangeLogForIssue(event)

if (!changelog) {
    return
}

def statusChanged = changelog.related("ChildChangeItem").any { item ->
    item.field == "status"
}

if (!statusChanged) {
    return
}

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

def message = [
    text: "Задача ${issue.key} изменила статус",
    attachments: [
        [
            color: "good",
            fields: [
                [title: "Задача", value: issue.key, short: true],
                [title: "Статус", value: issue.status.name, short: true],
                [title: "Исполнитель", value: issue.assignee?.displayName ?: "Не назначен", short: true]
            ]
        ]
    ]
]

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}")
}

Синхронизация с внешними системами

Синхронизируем данные задачи с внешней системой при изменении:

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 externalApiUrl = "https://external-api.company.com/issues/${issue.key}"

def payload = [
    key: issue.key,
    summary: issue.summary,
    status: issue.status.name,
    assignee: issue.assignee?.username,
    priority: issue.priority?.name,
    updated: new Date().toString()
]

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

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

try {
    def response = client.send(request, HttpResponse.BodyHandlers.ofString())
    if (response.statusCode() != 200) {
        log.error("Failed to sync issue ${issue.key}: ${response.statusCode()}")
    }
} catch (Exception e) {
    log.error("Failed to sync issue ${issue.key}: ${e.message}")
}

Предотвращение бесконечных циклов

Важно избегать бесконечных циклов. Например, если слушатель обновляет задачу, это может вызвать новое событие обновления. Используйте флаги или проверяйте источник события:

def event = event
def issue = event.issue

// Проверяем, что изменение сделано не системой
def user = event.user
if (user?.username == "system" || user == null) {
    return
}

// Используем кастомное поле для отслеживания автоматических изменений
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def autoUpdateField = customFieldManager.getCustomFieldObjectByName("Auto Updated")

if (issue.getCustomFieldValue(autoUpdateField) == "true") {
    // Это автоматическое обновление, пропускаем
    return
}

// Выполняем действия и устанавливаем флаг
def mutableIssue = ComponentAccessor.getIssueManager().getIssueObject(issue.id) as MutableIssue
mutableIssue.setCustomFieldValue(autoUpdateField, "true")

// Выполняем автоматическое действие...

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

// Сбрасываем флаг
mutableIssue.setCustomFieldValue(autoUpdateField, null)
ComponentAccessor.getIssueManager().updateIssue(currentUser, mutableIssue, 
    com.atlassian.jira.event.type.EventDispatchOption.DO_NOT_DISPATCH, false)

Производительность слушателей

Слушатели событий выполняются синхронно при каждом событии. Это может замедлить операции в Jira. Важные правила:

  • Выполняйте действия быстро — не делайте долгих операций в слушателях
  • Для долгих операций используйте асинхронность — запускайте задачи в фоне
  • Обрабатывайте ошибки — ошибки в слушателях могут блокировать операции
  • Фильтруйте события — обрабатывайте только нужные события

Типичные ошибки

Вот ошибки, которые часто допускают при работе со слушателями:

Ошибка 1: Бесконечные циклы

Слушатель обновляет задачу, что вызывает новое событие, которое снова вызывает слушатель. Используйте флаги или проверяйте источник события.

Ошибка 2: Долгие операции

Вызовы внешних API или сложные вычисления в слушателях замедляют операции в Jira. Используйте асинхронную обработку или очередь задач.

Ошибка 3: Игнорирование ошибок

Ошибки в слушателях могут блокировать операции. Всегда обрабатывайте исключения.

Выводы

Слушатели событий — мощный инструмент автоматизации в Jira. Они позволяют реагировать на события и выполнять автоматические действия: назначение, создание подзадач, синхронизацию с внешними системами, отправку уведомлений.

Всегда обрабатывайте ошибки, избегайте бесконечных циклов, помните о производительности. Для долгих операций используйте асинхронную обработку. Фильтруйте события, чтобы обрабатывать только нужные.

Тестируйте слушатели на тестовых проектах перед применением на production. Логируйте действия для отладки и мониторинга.

Если нужна помощь с созданием слушателей событий — свяжитесь со мной.