
В этой статье расскажу, как мы реализовали гибкое многоэтапное согласование в Jira. Особенность подхода - все согласование зациклено в одном статусе, без громоздких схем workflow. Вся логика задается в Assets и управляется через Groovy‑скрипт.
Постановка задачи
Бизнес хотел видеть:
несколько последовательных этапов согласования;
возможность задавать количество этапов, согласующих, и условия этапа ("все" или "любой");
единый прозрачный результат в виде таблицы в задаче;
автоматический переход задачи в "Согласовано" или возврат в "В работу".
Инструменты и подход
Assets (бывший Insight) - хранение конфигурации согласования (этапы, согласующие, условия).
Groovy - основная логика.
-
JSON - два скрытых поля:
ApprovalConfig
- структура согласования, формируется скриптом из Assets при старте процесса;ApprovalResult
- динамический результат согласования (кто согласовал/отклонил, на каком этапе).
Архитектура решения
Согласование зациклено в одном статусе:
Задача не прыгает по workflow - все крутится внутри "Согласование".-
Цепочка согласования:
Выбирается пользователем из списка преднастроеных цепочек через поле справочник Assets object на экране создания задачи или на переходе в согласование
-
Два скрытых поля:
ApprovalConfig
(JSON) - конфиг, формируется один раз на старте. Нужен чтобы скрипт переключал этапы.ApprovalResult
(JSON) - результаты согласования, обновляются на каждом этапе. Используется для отрисовки таблицы результатов
-
Работа с пользователями:
В поле "Необходимо согласование" заносятся все согласующие текущего этапа.
Когда пользователь согласовал он удаляется оттуда и переносится в поле "Согласовано".
При переходе на следующий этап список «Необходимо согласование» обновляется.
-
Условия этапа:
"Все должны согласовать" или "достаточно одного". Настраивается в Assets.
-
Результат на экране:
Отдельный UI‑блок в задаче — табличка: Этап | Пользователь | Статус («Согласовано» / «Отказано»).
Реализация
1. Формирование цепочки и этапов согласования
Настройки в Assets (этапы, согласующие, условие ALL/ANY)


2. Формирование конфига
Groovy-скрипт берет данные из Assets и строит JSON-конфиг.
def getAllStepsData(){
def result = [steps: []]
def stepCount = 1
//OBJECTID глобальная переменная, значение поля справочник Assets object
if (!OBJECTID) return null
//STEP_ATTR_IDS список из ID этапов на вкладке "цепочка согласования" в Assets
STEP_ATTR_IDS.each { stepAttrId ->
//assets - наша внутренняя библиотека реализующая
//методы пакета com.riadalabs.jira.plugins.insight
//в данном случае assets.getAttributeValue()
//соответствует методу ObjectFacade.loadObjectAttributeBean()
def stepIds = assets.getAttributeValue(OBJECTID, stepAttrId)
if (!stepIds) return
stepIds.each { stepId ->
def type = assets.getAttributeValue(stepId, 26627)?.getAt(0)
def status = assets.getAttributeValue(stepId, 26628)?.getAt(0)
if (status != 1) return // обрабатываем только активные
def approversList = []
def customApprover = assets.getAttributeValue(stepId, 26632)?.getAt(0)
def approvers = assets.getAttributeValue(stepId, 26626)
def stepName = assets.getAttributeValue(stepId, 26617)?.getAt(0)
if (customApprover) {
if (customApprover == "reporter") {
approversList << issue.reporter.key
}
} else if (approvers) {
approversList = approversList.plus(approvers)
}
if (approversList) {
def stageStatus = (result.steps.isEmpty())?"in progress":"awaiting"
result.steps << [
"step $stepCount": [
"approvers" : approversList,
"stepName" : stepName,
"type" : type,
"stepStatus" : stageStatus
]
]
}
stepCount++
}
}
return result
}
Итоговая структура записывается в поле ApprovalConfig
{
"steps": [
{
"step 1": {
"approvers": [
"fyulgushev",
"iivanov"
],
"stepName": "Согласующие первого этапа",
"type": "Все",
"stepStatus": "in progress"
}
},
{
"step 2": {
"approvers": [
"fyulgushev",
"ppetrov"
],
"stepName": "Согласующие второго этапа",
"type": "Любой",
"stepStatus": "awaiting"
}
},
{
"step 3": {
"approvers": [
"fyulgushev",
"ssidorov"
],
"stepName": "Согласующие третьего этапа",
"type": "Все",
"stepStatus": "awaiting"
}
},
{
"step 4": {
"approvers": [
"iivanov",
"ppetrov"
],
"stepName": "Согласующие четвертого этапа",
"type": "Все",
"stepStatus": "awaiting"
}
}
]
}
3. Инициализация согласования
При старте заполняется поле "Необходимо согласование" пользователями из первого этапа.
В ApprovalResult
создается структура для отрисовки таблицы согласования.
{
"steps": [
{
"approverName": "fyulgushev",
"dep": "Согласующие первого этапа",
"status": "moved"
},
{
"approverName": "iivanov",
"dep": "Согласующие первого этапа",
"status": "moved"
}
]
}
4. Обработка согласования
Когда пользователь нажимает "Согласовать" или "Отклонить":
его убираем из поля "Необходимо согласование";
переносим в поле "Согласовано" (если согласовал) или в поле "Отказано" (если отказал);
обновляем
ApprovalResult
.
def successJsonData(user){
JSONDATA["steps"].find{it["approverName"] == user.name && it.status == "moved"}.status = "success"
//NEEDAPPROVE глобальная переменная со значением поля "Необходимо согласование"
JSONDATA["steps"].removeIf { !(it["approverName"] in NEEDAPPROVE.collect{it.name}) && it["status"] == "moved"}
//updateJsonField записывает структуру в поле ApprovalResult
updateJsonField(ApprovalResultFieldID, JSONDATA)
}
5. Проверка условий этапа
Если "ALL" → ждем всех согласующих.
Если "ANY" → достаточно одного согласия.
Если условие выполнено → этап закрывается, и переходим к следующему.
6. Переходы задачи
Если все этапы пройдены → заявка переводится в статус "Согласовано".
Если хоть один отказал → заявка возвращается в статус "В работе".
Визуализация результата
В задаче пользователи видят таблицу (рендерится из ApprovalResult
):

JSONDATA = new JsonSlurper().parseText(DATA)
def jsonSteps = JSONDATA.steps
String html = """
<table class='aui myTable'>
"""
if (jsonSteps){
for (item in jsonSteps){
html += userData(item.approverName,item.dep,getStatus(item.status))
}
}
html += "</table>"
return html
def userData(userName,dep,status){
def user = getUser(userName)
AvatarService avatarService = ComponentAccessor.getAvatarService()
def userIconUrl = avatarService.getAvatarURL(ComponentAccessor.jiraAuthenticationContext.getLoggedInUser(), user.name).toString()
def urlUser = baseURL+"/secure/ViewProfile.jspa?name="
def tr = "<tr>"
tr += """
<th class="myTr">
<div>
<div style="float: left;">
<img src=${userIconUrl} style="border-radius:50%;vertical-align:middle; width="32"; height="32";">
</div>
<div style="margin-bottom: 10px; margin-left: 40px;">
<a href="${urlUser+user.name}">${user.displayName}</a>
</div>
</div>
</th>
"""
tr += """<th class="myTr">${dep}</th>"""
tr += """<th class="myTr">${status}</th>"""
tr += "</tr>"
return tr
}
def getStatus (key) {
def map = [
success : "<span style=\"font-size: 12px;\" class=\"aui-lozenge aui-lozenge-success\">Согласовано</span>",
moved : "<span style=\"font-size: 12px;\" class=\"aui-lozenge aui-lozenge-default\">Ожидает согласования</span>",
error : "<span style=\"font-size: 12px;\" class=\"aui-lozenge aui-lozenge-removed\">Отклонено</span>",
dialog : "<span style=\"font-size: 12px;\" class=\"aui-lozenge aui-lozenge-moved\">Обсуждение</span>"
]
return map.get(key)
}
Результат
Убрали громоздкий workflow.
Все согласование реализовано в одном статусе.
Настройки выносятся в Assets, администратор может управлять согласованием без переписывания скриптов.
Пользователи видят удобную табличку прогресса.
Ограничения и подводные камни
Придется поддерживать Groovy-код (проверка условий, обновление JSON, перезапуск согласования, обновление согласующих).
Визуализация требует кастомных скриптов.
Что можно улучшить
Объединить
ApprovalConfig и ApprovalResult
в единую структуруХранить JSON не в custom fields, а в issue properties
А как у вас организовано многоэтапное согласование? Используете Groovy/Assets или сторонние плагины с Marketplace?