Слушатели событий (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. Логируйте действия для отладки и мониторинга.
Если нужна помощь с созданием слушателей событий — свяжитесь со мной.