image

В крупных организациях часто возникает необходимость прикрутить к JIRA какой-либо дополнительный функционал, которого нет в стандартной поставке: автоматизацию, интеграцию с другими системами и прочие кастомизации. Зачастую это решается сторонними плагинами, в Atlassian Market их огромное количество. Но что делать, если подходящего плагина нет? Или он стоит 3000$, а вам нужна всего одна функция в нем? Очевидно, написать свой. Ещё один вариант для расширения — плагины, добавляющие возможность использовать свои скрипты в JIRA: ScriptRunner (Groovy), Jira Scripting Suite (SIL), JJupin (Jython).

В этой статье я расскажу о самом популярном и функциональном из них — ScriptRunner от Adaptavist.

ScriptRunner позволяет писать свои скрипты на Groovy, которые могут напрямую использовать JIRA Java API. Т.е. вам доступен практически такой же функционал, как и при создании своих плагинов. Только писать расширения на Groovy гораздо приятнее: никакой возни с xml-конфигами, maven-боли и проблем с зависимостями, только код. У JIRA довольно богатый и хорошо документированный Java API. В отличии от Rest API, на Java API можно сделать всё, что можно сделать через веб-интерфейс и даже больше. Groovy — это скриптовый язык с Java-подобным синтаксисом. Он компилируется в Java байткод и выполняется JVM, работает с Java библиотеками, как с родными, поддерживает одновременно статическую и динамическую типизацию и замыкания.

ScriptRunner


Начиная с третьей версии плагина все скрипты должны находиться в одной определённой директории (%scriptroot%). По умолчанию это директория <jira-app-dir>/scripts. Изменить её можно, отредактировав параметр -Dplugin.script.roots в jvm args, вот так:
-Dplugin.script.roots="/home/jira/jira-data/scripts/"
Подробнее о том, как добавить или изменить параметры в jvm args можно прочитать здесь.

В ScriptRunner есть несколько основных типов кастомных скриптов, различающихся по назначению и применению:

PostFunction. Выполняется после перевода issue в другой статус. Привязывается к конкретному переходу.
Validator. Валидация данных при попытке перевести issue в другой статус. Разрешает или запрещает перевод статуса. Привязывается к конкретному переходу.
Conditions. Логическое выражение, определяющее, можно ли сделать перевод статуса. Если нельзя — скрывает кнопку для изменения статуса. Привязывается к конкретному переходу.
Listener. Реагирует на события IssueEvent. Например, на создание или обновление issue. Можно указать, на какие события реагировать и в каких проектах.
JQL Functions. JQL-функции для поиска issue.

Любой скрипт можно выполнить прямо в окне браузера, через админ-панель, что очень удобно для тестирования небольших скриптов. Давайте уже напишем первый скрипт и запустим его. Установите плагин на свой тестовый инстанс JIRA, если вы этого ещё не сделали. Зайдите в Administration > Add-ons > ScriptRunner > Script Console. Как видите, кнопка Run недвусмысленно намекает, что код в консоли можно запустить прямо отсюда. Хвала богам, в третьей версии окно консоли получило подсветку синтаксиса и ошибиться стало чуть сложнее! Обратите внимание на закладки File / Script справа: можно выполнить скрипт, указав к нему путь относительно %scriptroot%. Вставьте в окно вот такой код, заменив ключ issue на реально существующий в вашем инстансе:
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.MutableIssue

IssueManager issueManager = ComponentAccessor.getIssueManager()

// Замените PRJ-1 на ключ существующего issue.
MutableIssue curIssue = issueManager.getIssueObject("PRJ-1")
String result = curIssue.key + ": " + curIssue.summary

В результате работы скрипта выводится содержимое последней переменной в скрипте, в данном случае это result.


Настройка рабочего окружения


Этот раздел можно пропустить, если вы собираетесь писать только совсем небольшие скрипты. В других случаях, конечно, удобнее будет вести разработку в IDE с автодополнением кода, дебаг-режимом, системой контроля версий и прочими плюшками. В качестве IDE нам подойдет IntelliJ IDEA, она поддерживает Groovy прямо из коробки. Итак, нам нужно установить IntelliJ IDEA (подойдет Community Edition), Java SDK и Groovy. Установите IDEA и создайте новый проект (File > New Project). Выберите Groovy проект, укажите путь к JDK и к Groovy.
New Project

Теперь нам нужно подключить библиотеки JIRA, чтобы было доступно автодополнение кода. Самый простой вариант — скопировать к себе папки classes и lib из <jira-app-dir>/atlassian-jira/WEB-INF. Самый лучший вариант — скачать исходники с официального сайта. Откройте настройки проекта: File > Project Structure > Project Settings > Libraries. Добавьте папки classes и lib к проекту:
Project Libraries

Теперь давайте настроим удаленную отладку. Чтобы включить дебаг режим в JIRA, нужно к jvm args добавить параметр:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
После этого откройте настройку конфигураций Run > Edit Configurations, добавьте новый Remote Config. На вкладке Configuration введите имя конфигурации, например, «Jira Debug» и укажите хост, на котором находится ваша JIRA. Естественно, номер порта для отладки должен быть таким же, каким вы указали его в параметре address в jvm args.

Проверяем. Создайте скрипт в корне вашего проекта. Скопируйте этот скрипт на JIRA-инстанс, в папку %scriptroot%. Запустите отладку: нажмите Run > Debug 'Jira Debug'. Вы увидите сообщение: Connected to the target VM, address: %your_host%, transport: 'socket'. В Script Console введите имя вашего скрипта и нажмите Run. Вы должны увидеть примерно такую картину:
Groovy Debugging

Имейте ввиду, что JIRA не будет отвечать, пока вы не отпустите отладку, т.е. вам точно нужен отдельный инстанс для дебага. Кстати, в IDEA для проверки Groovy-кода можно использовать Groovy Console (Tools > Groovy Console).

Скрипты


Для работы с сущностями в JIRA API используются компоненты с соответствующими именами, вот некоторые из них: IssueManager, ProjectManager, UserManager, SearchService, CustomFieldManager, CommentManager. Их экземпляры можно получить через ComponentAccessor, ниже вы увидите это в примерах. В поисках нужного метода вам поможет IDEA с автодополнением кода, а с описанием — документация JIRA Java API. Узнать ID какой-либо сущности можно через интерфейс самой JIRA, просто найдите ссылки для её редактирования или просмотра, в большинстве случаев в url будет содержаться нужный вам ID. Что примечательно, в сети очень много примеров работы с JIRA на Java и практически всегда этот код будет работать на Groovy без изменений.

Пожалуй, самой распространенной задачей в скриптах будет изменение issue. Давайте напишем код, который меняет summary и какое-нибудь кастомное поле.
import com.atlassian.crowd.embedded.api.User
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.type.EventDispatchOption
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder

// Получаем компоненты-менеджеры
ComponentManager componentManager = ComponentManager.getInstance()
IssueManager issueManager = ComponentAccessor.getIssueManager()
CustomFieldManager customFieldManager = componentManager.getCustomFieldManager()

// Текущий пользователь
User curUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Получаем issue по его ключу
MutableIssue curIssue = issueManager.getIssueObject("PRJ-1")

// 1. Обновляем значение системного поля Summary.
// Присваиваем новое значение поля в объекте issue.
curIssue.summary = "New summary"

// Записываем изменения issue в Jira.
// Обратите внимание, что при этом будет вызван event IssueUpdated, на который среагируют лиссенеры, если таковые имеются.
// Также произойдет реиндекс обновленного issue, обновится история issue, и новое значение поля будет сразу видно в интерфейсе.
issueManager.updateIssue(curUser, curIssue, EventDispatchOption.ISSUE_UPDATED, false)

// 2. Обновляем значение кастомного поля. (1)
// Получаем кастомное поле по имени.
CustomField cfReleaseVersion = customFieldManager.getCustomFieldObjectByName("Release Version")

// Обновление значения с записью в историю и вызовом event
curIssue.setCustomFieldValue(cfReleaseVersion, "3.5")
issueManager.updateIssue(curUser, curIssue, EventDispatchOption.ISSUE_UPDATED, false)

// 3. Обновляем значение кастомного поля. (2)
// Получаем кастомное поле по id.
CustomField cfUpdateVersion = customFieldManager.getCustomFieldObject("customfield_13500")

// "Тихое" обновление кастомного поля, оно не вызовет event и не будет отражено в истории issue.
cfUpdateVersion.updateValue(null, curIssue, new ModifiedValue(null, "2.4"), new DefaultIssueChangeHolder())

Далее парочка примеров с комментариями.
Поиск issue по JQL запросу
import com.atlassian.crowd.embedded.api.User
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.web.bean.PagerFilter

IssueManager issueManager = ComponentAccessor.getIssueManager()
SearchService searchService = ComponentAccessor.getComponent(SearchService.class)
User curUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// JQL-запрос для поиска issue, созданных за последние два дня
String JqlQuery = "created > -2d"

def parseResult = searchService.parseQuery(curUser, JqlQuery)
def searchResult = searchService.search(curUser, parseResult.getQuery(), PagerFilter.getUnlimitedFilter())
def IssuesByJql = searchResult.issues.collect { issueManager.getIssueObject(it.id) }

return IssuesByJql

Создание линка между issue
import com.atlassian.crowd.embedded.api.User
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.link.IssueLinkManager
import com.atlassian.jira.issue.link.IssueLinkType
import com.atlassian.jira.issue.link.IssueLinkTypeManager

IssueManager issueManager = ComponentAccessor.getIssueManager()
IssueLinkManager issueLinkManager = ComponentManager.getInstance().getIssueLinkManager()
IssueLinkTypeManager issueLinkTypeManager = (IssueLinkTypeManager) ComponentManager.getComponentInstanceOfType(IssueLinkTypeManager.class)
User curUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

// Получаем тип линка по имени
IssueLinkType duplicateLinkType = issueLinkTypeManager.getIssueLinkTypesByName("Duplicate")?.first()

if (duplicateLinkType != null) {
    def issue_1 = issueManager.getIssueObject("PRJ-1")
    def issue_2 = issueManager.getIssueObject("PRJ-2")

    // Создаем линк PRJ-1 duplicates PRJ-2
    issueLinkManager.createIssueLink(issue_1.id, issue_2.id, duplicateLinkType.id, null, curUser)
}


В WorkFlow-скриптах (PostFunction, Validator, Condition) доступ к текущему issue можно получить через переменную issue, она биндится ScriptRunner-ом. Лиссенеры организованы чуть по-другому. Вам нужно создать свой класс, наследующий AbstractIssueEventListener.
Простейший лиссенер выглядит вот так:
package Listeners // лиссенер должен находиться в папке %scriptroot%/Listeners

import com.atlassian.jira.event.issue.AbstractIssueEventListener
import com.atlassian.jira.event.issue.IssueEvent
import com.atlassian.jira.issue.Issue
import org.apache.log4j.Level
import org.apache.log4j.Logger

class SimpleListener extends AbstractIssueEventListener {
    Logger log = Logger.getLogger(this.class.simpleName)

    @Override
    void workflowEvent(IssueEvent event) {
        this.customEvent(event)
    }

    @Override
    void customEvent(IssueEvent event) {
        Issue curIssue = event.issue

        log.setLevel(Level.DEBUG)
        log.debug("Event catch: ${event.eventTypeId} fired for ${curIssue.key} (${curIssue.issueTypeObject.name}).")
    }
}

Заключение


На Groovy вы можете выполнять http-запросы, читать данные из БД, отправлять письма, использовать JIRA Java API и стандартные Java библиотеки. В самом ScriptRunner еще много возможностей, о которых я не упомянул в статье: scripted fields, юнит-тесты, встроенные скрипты и многое другое. К сожалению, начиная с 4-й версии, плагин стал платным. Если вы планируете обновлять JIRA до 7 версии (или уже её используете), учтите, что на неё можно поставить только четвертую платную версию ScriptRunner. Но все основные функции доступны во 2-й и 3-й бесплатных версиях. Приведённые выше скрипты проверялись на JIRA 6.4.12 (Java 8) и ScriptRunner 3.0.16.

Ресурсы


Официальный сайт ScriptRunner
ScriptRunner на Atlassian Marketplace
Официальный сайт Groovy
Документация JIRA API
Статья Rocking With Jira ScriptRunner (осторожно, местами устаревший код)
Старая документация ScriptRunner (для версий до 3.0)

Комментарии (5)


  1. erthad
    18.01.2016 18:36

    В JIRA Cloud вот только не работает. Есть что-то похожее для Cloud?


  1. SlavikF
    19.01.2016 00:05
    +1

    Как-то не логично вы рекламируете:

    Но что делать, если подходящего плагина нет? Или он стоит 3000$, а вам нужна всего одна функция в нем?
    … вариант… — ScriptRunner (Groovy)

    И этот ScriptRunner, вдруг тоже стоит $3000+ (на соответствующих инсталяциях).

    И зачем тогда весь огород городить?

    Сам ScriptRunner, конечно неплох. Но вот такое аргументирование…


    1. fixer_m
      19.01.2016 00:33

      Вторая и третья версии бесплатны и устанавливаются на Jira 5 и 6.


      1. brate1nikoff
        19.01.2016 11:05

        Но на JIRA 7, потихоньку надо будет переходить.


        1. fixer_m
          19.01.2016 11:18

          Согласен. На счет JIRA 7 я честно предупредил в заключении.