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

Автоматизация workflow в Jira через ScriptRunner

Workflow — это сердце процесса работы с задачами в Jira. Стандартные возможности workflow мощные, но часто их недостаточно для сложных бизнес-процессов. ScriptRunner позволяет создавать кастомные валидаторы, пост-функции и условия перехода, которые открывают практически безграничные возможности автоматизации. В этой статье разберу практические примеры автоматизации workflow.

Компоненты workflow в ScriptRunner

ScriptRunner позволяет создавать три типа кастомных компонентов для workflow:

  • Валидаторы — проверяют условия перед переходом по статусу
  • Пост-функции — выполняют действия после перехода
  • Условия перехода — определяют, доступен ли переход для конкретного пользователя

Каждый компонент — это Groovy-скрипт, который получает контекст перехода и может выполнять любые действия через API Jira.

Создание кастомных валидаторов

Валидаторы проверяют условия перед переходом. Если валидация не проходит, переход блокируется и показывается сообщение об ошибке.

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

Стандартные валидаторы Jira могут проверить, что поле заполнено, но не могут проверить сложные условия. Например, нужно проверить, что поле "Приоритет" заполнено только для задач типа Bug:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.opensymphony.workflow.InvalidInputException

def customFieldManager = ComponentAccessor.getCustomFieldManager()
def priorityField = customFieldManager.getCustomFieldObjectByName("Приоритет")

def issue = issue
def priority = issue.getCustomFieldValue(priorityField)

if (issue.issueType.name == "Bug" && !priority) {
    throw new InvalidInputException("Для задач типа Bug необходимо указать приоритет")
}

Валидатор в ScriptRunner получает объект issue автоматически. Если валидация не проходит, выбрасывается InvalidInputException с сообщением для пользователя.

Пример: проверка связанных задач

Можно проверить, что все связанные задачи закрыты перед закрытием текущей:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.link.IssueLinkManager
import com.opensymphony.workflow.InvalidInputException

def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def links = issueLinkManager.getInwardLinks(issue.id)

def openLinkedIssues = links.findAll { link ->
    def linkedIssue = link.getSourceObject()
    linkedIssue.resolution == null
}

if (openLinkedIssues) {
    def keys = openLinkedIssues.collect { it.getSourceObject().key }.join(", ")
    throw new InvalidInputException("Нельзя закрыть задачу, пока не закрыты связанные задачи: ${keys}")
}

Создание кастомных пост-функций

Пост-функции выполняются после успешного перехода. Они могут изменять задачу, создавать новые задачи, отправлять уведомления, вызывать внешние API и многое другое.

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

Автоматически назначаем задачу на разработчика соответствующего компонента:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.user.ApplicationUser

def issueManager = ComponentAccessor.getIssueManager()
def userManager = ComponentAccessor.getUserManager()

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

// Маппинг компонентов на пользователей
def componentToUserMap = [
    "Frontend": "frontend.dev",
    "Backend": "backend.dev",
    "Database": "db.admin"
]

def assignee = null
components.each { component ->
    def userName = componentToUserMap[component.name]
    if (userName) {
        assignee = userManager.getUserByKey(userName)
    }
}

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

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

При переходе задачи в статус "In Development" автоматически создаём подзадачи для каждого компонента:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueFactory
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.project.Project

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

def components = issue.components

components.each { component ->
    def subTask = issueFactory.getIssue()
    subTask.setProjectObject(issue.projectObject)
    subTask.setIssueTypeId("5") // ID типа подзадачи
    subTask.setParentId(issue.id)
    subTask.setSummary("Разработка компонента: ${component.name}")
    subTask.setReporter(currentUser)
    
    def createdIssue = issueManager.createIssueObject(currentUser, subTask)
    log.warn("Created sub-task: ${createdIssue.key}")
}

Пример: обновление родительской задачи

При закрытии всех подзадач автоматически переводим родительскую задачу в статус "Done":

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.workflow.WorkflowManager

def issueManager = ComponentAccessor.getIssueManager()
def workflowManager = ComponentAccessor.getWorkflowManager()

if (issue.parentObject) {
    def parentIssue = issue.parentObject
    def subTasks = issueManager.getSubTaskObjects(parentIssue.id)
    
    def allSubTasksClosed = subTasks.every { subTask ->
        subTask.resolution != null
    }
    
    if (allSubTasksClosed && parentIssue.status.name != "Done") {
        def parentMutable = issueManager.getIssueObject(parentIssue.id) as MutableIssue
        def workflow = workflowManager.getWorkflow(parentMutable)
        def action = workflow.getLinkedStatus(parentMutable.status.id)?.getActions().find { 
            it.getName() == "Close Issue"
        }
        
        if (action) {
            def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
            workflowManager.transitionIssue(currentUser, parentMutable, action.getId(), null)
        }
    }
}

Создание условий перехода

Условия перехода определяют, виден ли переход конкретному пользователю. Полезно для скрытия переходов на основе сложных условий.

Пример: переход доступен только автору задачи

Переход "Reopen" доступен только автору задачи:

import com.atlassian.jira.component.ComponentAccessor

def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

if (!currentUser) {
    return false
}

return issue.reporter == currentUser

Пример: переход доступен только администраторам проекта

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.security.roles.ProjectRoleManager

def projectRoleManager = ComponentAccessor.getComponent(ProjectRoleManager)
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

if (!currentUser) {
    return false
}

def adminRole = projectRoleManager.getProjectRole("Administrators")
def hasRole = projectRoleManager.isUserInProjectRole(currentUser, adminRole, issue.projectObject)

return hasRole

Работа с комментариями и историей

В пост-функциях можно автоматически добавлять комментарии:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.comments.CommentManager

def commentManager = ComponentAccessor.getCommentManager()
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

def comment = "Задача автоматически переведена в статус ${transition.name}."
commentManager.create(issue, currentUser, comment, false)

Интеграция с внешними системами

Пост-функции могут вызывать внешние API. Например, отправлять уведомления в 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 slackWebhookUrl = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

def message = [
    text: "Задача ${issue.key} переведена в статус ${transition.name}",
    attachments: [
        [
            color: "good",
            fields: [
                [title: "Задача", value: issue.key, short: true],
                [title: "Статус", value: transition.name, 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 {
    def response = client.send(request, HttpResponse.BodyHandlers.ofString())
    log.warn("Slack notification sent: ${response.statusCode()}")
} catch (Exception e) {
    log.error("Failed to send Slack notification: ${e.message}")
}

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

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

Ошибка 1: Блокирующие операции

Валидаторы и условия должны выполняться быстро. Не делайте долгих операций (вызовы внешних API, сложные запросы к БД) в валидаторах — это замедлит workflow.

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

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

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

Пост-функция, которая переводит задачу в другой статус, может вызвать бесконечный цикл, если в новом статусе тоже есть такая пост-функция. Будьте осторожны.

Выводы

ScriptRunner открывает огромные возможности для автоматизации workflow. Кастомные валидаторы, пост-функции и условия перехода позволяют реализовать практически любой бизнес-процесс.

Начинайте с простых скриптов, тестируйте их на тестовых проектах, постепенно усложняйте. Помните о производительности — валидаторы должны быть быстрыми, пост-функции не должны блокировать пользователей.

Документируйте созданные скрипты — через полгода будет сложно вспомнить логику. Используйте комментарии в коде и ведите документацию.

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