JIRA: Как мы сделали многоэтапное согласование в одном статусе (Groovy + Assets)

от автора

В этой статье расскажу, как мы реализовали гибкое многоэтапное согласование в Jira. Особенность подхода — все согласование зациклено в одном статусе, без громоздких схем workflow. Вся логика задается в Assets и управляется через Groovy‑скрипт.

Постановка задачи

Бизнес хотел видеть:

  • несколько последовательных этапов согласования;

  • возможность задавать количество этапов, согласующих, и условия этапа («все» или «любой»);

  • единый прозрачный результат в виде таблицы в задаче;

  • автоматический переход задачи в «Согласовано» или возврат в «В работу».

Инструменты и подход

  • Assets (бывший Insight) — хранение конфигурации согласования (этапы, согласующие, условия).

  • Groovy — основная логика.

  • JSON — два скрытых поля:

    1. ApprovalConfig — структура согласования, формируется скриптом из Assets при старте процесса;

    2. ApprovalResult — динамический результат согласования (кто согласовал/отклонил, на каком этапе).

Архитектура решения

  1. Согласование зациклено в одном статусе:
    Задача не прыгает по workflow — все крутится внутри «Согласование».

  2. Цепочка согласования:

    Выбирается пользователем из списка преднастроеных цепочек через поле справочник Assets object на экране создания задачи или на переходе в согласование

  3. Два скрытых поля:

    • ApprovalConfig (JSON) — конфиг, формируется один раз на старте. Нужен чтобы скрипт переключал этапы.

    • ApprovalResult (JSON) — результаты согласования, обновляются на каждом этапе. Используется для отрисовки таблицы результатов

  4. Работа с пользователями:

    • В поле «Необходимо согласование» заносятся все согласующие текущего этапа.

    • Когда пользователь согласовал он удаляется оттуда и переносится в поле «Согласовано».

    • При переходе на следующий этап список «Необходимо согласование» обновляется.

  5. Условия этапа:

    • «Все должны согласовать» или «достаточно одного». Настраивается в Assets.

  6. Результат на экране:

    • Отдельный 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):

Таблица с результатом согласования - это scripted field

Таблица с результатом согласования — это scripted field
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?


ссылка на оригинал статьи https://habr.com/ru/articles/940626/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *