Настройка Jira под ваши нужды. Cовершенный флоу и идеальный тикет

ff9xb7hgirmgmbwdt1an6w-pzpa.png


Если вы работаете в IT-компании, то, скорее всего, ваши процессы построены вокруг известного продукта Atlassian — Jira. На рынке есть множество таск-трекеров для решения тех же задач, в том числе open-source-решения (Trac, Redmine, Bugzilla), но, пожалуй, именно Jira имеет сегодня самое широкое распространение.

Меня зовут Дмитрий Семенихин, я тимлид компании Badoo. В небольшом цикле статей я расскажу, как именно мы используем Jira, как настраивали её под свои процессы, что хорошего «прикрутили» сверху и как тем самым превратили issue-трекер в единый центр коммуникаций по задаче и упростили себе жизнь. В этой статье вы увидите наш флоу изнутри, узнаете, как можно «докрутить» свою Jira, и прочтёте о дополнительных возможностях инструмента, о которых могли не знать.

Статья ориентирована прежде всего на тех, кто уже использует Jira, но, возможно, испытывает сложности с интеграцией её стандартных возможностей в существующие в компании процессы. Также статья может быть полезна компаниям, которые используют другие таск-трекеры, но столкнулись с некоторыми ограничениями и подумывают о смене решения. Статья построена не по принципу «проблема — решение», в ней я описываю сложившийся инструментарий и фичи, построенные нами вокруг Jira, а также технологии, которые мы использовали для их реализации.


Чтобы последующий текст был более понятным, давайте разберёмся, какие инструменты предоставляет нам Jira для реализации нестандартных хотелок — тех, что выходят за рамки стандартного функционала Jira.

REST API


В общем случае вызов команды API — это HTTP-запрос к URL API с указанием метода (GET, PUT, POST and DELETE), команды и тела запроса. Тело запроса, а также ответ API —  в JSON-формате. Пример запроса, который вернёт JSON-представление тикета:

GET /rest/api/latest/issue/{ticket_number}


С помощью API вы можете, используя скрипты на любом языке программирования:

  • создавать тикеты;
  • модифицировать любые свойства тикетов (встроенные и кастомные);
  • писать комментарии;
  • с помощью JQL (встроенный язык запросов) получать любые списки тикетов;
  • и многое другое.


Подробная документация об API представлена по ссылке.

Мы написали собственный высокоуровневый Jira API-клиент на PHP, который реализует все необходимые нам команды. Вот пример команд для работы с комментариями:

public function addComment($issue_key, $comment)
{
   return $this->_post("issue/{$issue_key}/comment", ['body' => $comment]);
}
 
public function updateComment($issue_key, $comment_id, $new_text)
{
   return $this->_put("issue/{$issue_key}/comment/{$comment_id}", ['body' => $new_text]);
}
 
public function deleteComment($issue_key, $comment_id)
{
   return $this->_delete("issue/{$issue_key}/comment/{$comment_id}");
}


Webhooks


С помощью webhook можно настроить вызов внешней callback-функции на вашем хосте на различные события в Jira. При этом можно настроить сколько угодно таких правил таким образом, что различные URL будут «дёргаться» для разных событий и для тикетов, которые соответствуют указанному в webhook фильтру. Интерфейс настройки webhooks доступен администратору Jira.

В результате можно создавать правила вроде этого:

Name: «SRV — New Feature created/updated»
URL: www.myremoteapp.com/webhookreceiver
Scope: Project = SRV AND type in («New Feature»)
Events: Issue Updated, Issue Created

В данном примере указанный URL будет вызываться для событий создания и изменения тикетов, соответствующих фильтру Scope. При этом в теле запроса будет содержаться вся необходимая информация о том, что именно изменилось и какое событие произошло.

Тут важно понимать, что Jira не гарантирует, что ваше событие будет доставлено. Если внешний URL не ответил или ответил с ошибкой, это нигде видно не будет (кроме логов, пожалуй). Поэтому обработчик событий webhook должен быть максимально надёжным. Например, события можно складывать в очередь и пытаться обработать до тех пор, пока это не закончится успехом. Это поможет решить проблемы с временно недоступными сервисами, например, какой-либо внешней базой данных, необходимой для правильной обработки события.

Подробная документация о webhooks представлена по ссылке.

ScriptRunner


Это плагин к Jira, очень мощный инструмент, который позволяет кастомизировать в Jira очень многое (в том числе он способен заменить собой webhooks). Для пользования этим плагином требуется знание Groovy. Основное преимущество инструмента для нас состоит в том, что можно встраивать во флоу кастомную логику в режиме онлайн. Код вашего скрипта будет исполняться сразу в среде Jira в ответ на определённое действие. Например, можно сделать в интерфейсе тикета свою кнопку, клик по которой будет создавать связанные с текущей задачей тикеты или запускать юнит-тесты для данной задачи. И если вдруг что-то пойдёт не так, вы как пользователь сразу об этом узнаете.

Желающие могут ознакомиться с документацией.


А теперь о том, как мы применяем дополнительные возможности Jira в наших проектах.  Рассмотрим это в контексте прохождения нашего типичного тикета по флоу от создания до закрытия. Заодно и про сам флоу расскажу.

Open/Backlog


Итак, сначала тикет попадает в беклог новых тикетов со статусом Open. Далее лид компонента, увидев новый тикет на своём дашборде, принимает решение: назначить тикет прямо сейчас разработчику либо отправить его в беклог известных тикетов (статус Backlog), чтобы назначить его позже, когда появится свободный разработчик и более приоритетные тикеты будут закрыты. Это может показаться странным, так как кажется логичным делать наоборот: создавать тикеты в статусе Backlog, а потом переводить в статус Open. Но у нас прижилась именно эта схема. Она позволяет легко настроить фильтры, чтобы сократить время принятия решения по новым тикетам. Пример JQL-фильтра, который показывает новые задачи лиду:

Project = SRV AND assignee is EMPTY AND status in (Open)

In Progress


Технические нюансы работы с Git
Надо отметить, что у нас работа над каждой задачей ведётся в отдельной Git-ветке. Насчёт этого у нас есть соглашение, что имя ветки в начале должно содержать номер тикета. Например, SRV-123_new_super_feature. Также комментари к каждому коммиту в ветку должны содержать номер тикета в формате [SRV-123]: {comment}. Такой формат необходим нам, например, для корректного удаления «плохой» задачи из билда. Как это делается, подробно описано в статье.

Эти требования контролируются Git-хуками. Например, вот содержимое prepare-commit-msg, который подготавливает комментарий к коммиту, получая номер тикета из имени текущей ветки:

#!/bin/bash
b=`git symbolic-ref HEAD| sed -e 's|^refs/heads/||' | sed -e 's|_.*||'`
c=`cat $1`
if [ -n "$b" ] && [[ "$c" != "[$b]:"* ]]
then
   echo "[$b]: $c" > $1
fi


Если коммит с «неправильным» комментарием попытаться запушить, такой пуш будет отклонён. Также отклонена будет попытка запушить ветку без номера тикета в начале.


Когда тикет попадает на разработчика, первым делом он декомпозируется. Результатом декомпозиции является представление разработчика о способах решения задачи и о том, сколько времени займёт решение. После того как все основные детали выяснены, тикет переводится в статус In Progress, а разработчик начинает писать код.

У нас принято выставлять задаче due date в момент, когда она переводится в статус In Progress. Если же разработчик этого не сделал, ему придёт напоминание в корпоративный мессенджер HipChat. Специальный скрипт раз в два часа:

  • выбирает с помощью REST API Jira тикеты в статусе in progress с пустым полем due date (project = SRV AND status = «In Progress» AND duedate is EMPTY);
  • выбирает незавершённые тикеты с due date старше текущей даты (project = SRV AND status = «In Progress»  AND duedate is not EMPTY AND duedate < now());
  • для каждого тикета узнаёт разработчика, читая соответствующее поле в тикете, а также лида разработчика;
  • группирует тикеты по разработчикам и лидам и отправляет напоминания в HipChat, используя его API.


Сделав все необходимые коммиты, разработчик пушит ветку в общую репу. В этом случае срабатывает Git-хук post-receive, который делает много всего интересного:

  • имя Git-ветки, а также комментарии к коммитам проверяются на соответствие нашим правилам;
  • проверяется, что тикет, с которым ассоциируется ветка, не закрыт (в закрытые тикеты пушить новый код нельзя);
  • проверяется синтаксис изменённых PHP-файлов (PHP -l file_name.php);
  • проверяется форматирование;
  • если тикет, в который пушится ветка, находится в статусе Open, то он автоматически переводится в статус In Progress;
  • тикет привязывается к ветке, делается соответствующая запись в кастомном поле тикета Commits с помощью Jira API. Выглядит это так:


snyt-ud30nzzbgr4seaehcccnme.png


(branchdiff — это ссылка на diff ветки с головой, от которой взяла своё начало текущая ветка, в нашем инструменте ревью кода Codeisok);

  • создаётся комментарий в тикете со всеми коммитами в данном пуше.
    5pnp284b9eobeeyafn3hfjgcimu.png

    (Aida — это условное название нашего комплекса автоматизации для работы с Jira, Git и не только. Именно от этого имени появляются автоматические комментарии в тикете. Подробнее об Aida мы писали в статье).
    Клик по хешу коммита открывает diff с предыдущей ревизией ветки (как это примерно выглядит, покажу ниже);
  • проверяется, есть ли в ветке файлы, для которых может потребоваться перевод на поддерживаемые языки (например, шаблоны web-страниц), и если такие есть, то кастомному полю тикета Lexems проставляется значение New\Changed. Это гарантирует, что тикет не уедет на продакшен без законченного перевода;
  • в список девелоперов (кастомное поле тикета Developers) добавляется имя сотрудника, который пушит ветку.


On Review


Написав код и самостоятельно убедившись, что все требования к задаче выполнены, а тесты не сломаны, разработчик назначает тикет ревьюверу (статус On Review). Обычно разработчик сам решает, кто будет ревьювить его тикет. Скорее всего, это будет другой разработчик, который отлично разбирается в нужной части кода. Ревью происходит с помощью инструмента Codeisok, который открывается сразу с нужным diff по клику на ссылку branchdiff в поле тикета Commits или на ссылку в виде хеша коммита в комментариях.

Ревьювер видит примерно такую картину:

tyegti15pqhzbdgiebcw1evm8pi.png


Закончив ревью, ревьювер нажимает кнопку Finish, и, помимо всего прочего, в этот момент происходит следующее:

  • с помощью API JIra создаётся комментарий в тикете с замечаниями ревьювера в контексте кода. Выглядит это примерно так:


0kn6kaydki4umhdxzmjebixy6eu.png

  • если замечания к коду были и ревьювер решил переоткрыть тикет, то разработчику придёт уведомление об этом в HipChat (это делается с помощью правила webhook, которое срабатывает на переоткрытие);
  • заполняется поле тикета Reviewers.


Resolved


Далее, если ревью прошло успешно, тикет отправляется в беклог QA-инженеров в статусе Resolved. Но вместе с этим с помощью webhook на событие resolved в фоне запускаются автоматические тесты на коде ветки. Спустя несколько минут в тикете появится новый комментарий, который сообщит о результатах тестов.

umffccnuyujwg3ypdz3scdcrrou.png

Также в любой момент можно вручную инициировать повторный прогон тестов, кликнув по специальной кнопке Run unit tests в меню тикета. После успешного прогона в тикете появится новый комментарий, аналогичный предыдущему.

ttsk65fr3egs9fbjpa7v6pbdzta.png


По сути, эта кнопка — один из дополнительных статусов задачи в workflow Jira, перевод в который инициирует срабатывание скрипта на Groovy для плагина ScriptRunner. Скрипт вызывает внешний URL, который инициирует прогон тестов, и если URL ответил успехом, то тикет возвращается в предыдущий статус (в нашем случае Resolved).

In Shot / In Shot — OK


Задача сначала тестируется в devel-окружении. Если всё хорошо, создаётся шот (например, кликом по ссылке Create shot в поле Commits) — директория на выделенном сервере, в которую копируются изменения из тикета, смёрженные с текущим master. Сервер работает с продакшен-данными: базы и сервисы те же, что обслуживают реальных пользователей. Таким образом, тестировщик может открыть web-сайт или подключиться к шоту с помощью мобильного клиента и «изолированно» проверить фичу в продакшен-окружении. «Изолированно» значит, что никакой другой код/функционал, кроме нового из ветки и текущего master, не исполняется. Поэтому этот этап тестирования является, пожалуй, основным, так как позволяет QA-инженеру максимально достоверно найти проблему непосредственно в тестируемой задаче.

Доступ к ресурсам шота осуществляется по специальным URL, которые генерируются в скрипте создания шота и с помощью API Jira помещаются в шапку тикета. В результате мы видим ссылки на сайт, админку, логи и прочие инструменты, которые исполняются в шот-окружении:

kfyke_qa3yjtezpyqx0-edwghfw.png

Также в момент генерации шота запускается скрипт, который анализирует содержимое изменённых файлов и создаёт заявки на перевод найденных новых лексем. После того как перевод закончен, значение поля Lexems меняется на Done и тикет может быть добавлен в билд.

Если тестирование в шоте прошло успешно, то тикет переводится в статус In Shot — OK.

In Build / In Build — OK


Мы выкладываем код два раза в день — утром и вечером. Для этого создаётся специальная build-ветка, которая в итоге будет слита с master и выложена «в бой».

В момент сборки build-ветки специальный скрипт с помощью JQL-запроса получает список тикетов в статусе In Shot — OK и пытается замёржить их в ветку билда при выполнении всех перечисленных ниже условий:

  • перевод для тикета закончен или переводить ничего не нужно (Lexems in («No», «Done»));
  • разработчик присутствует на рабочем месте (система автоматического слияния проверяет по внутренней базе, не находится ли разработчик в отпуске или на больничном, и если да, то тикет может быть замёржен только вручную релиз-инженерами или другим ответственным разработчиком, который указан в специальном поле Vice Developer; лид отсутствующего разработчика в этом случае получает уведомление о том, что тикет не может быть автоматически добавлен в билд);
  • у тикета не установлен флажок Up in Build в значение by Developer (это специальное кастомное поле тикета, которое даёт возможность разработчику самому определять, когда тикет попадёт в билд);
  • ветка тикета не зависит от другой ветки, которая ещё не попала в master или текущий билд. Мы всячески стараемся избегать подобной ситуации, но иногда такое происходит, когда разработчик создаёт свою ветку не от master, а от ветки другого тикета, либо когда вмёрживает к себе чужую ветку. Это можно сделать в том числе и случайно, поэтому мы решили, что дополнительная защита не помешает.


Стоит отметить, что автоматическое слияние может не произойти по причине конфликта слияния. В этом случае тикет автоматически переводится в статус Reopen и назначается разработчику, о чём он немедленно получает оповещение в HipChat, а в комментарий тикета добавляется соответствующее сообщение. После разрешения конфликта тикет возвращается в билд.

Если же всё хорошо и ветка тикета замёржилась в билд, тикет автоматически переводится в статус In Build, а в кастомное поле тикета Build_Name пишется название билда.

teblcqsgvc0tgn82svazpssgmii.png


Далее, используя это значение, легко получить список тикетов, которые были выложены с каждым билдом. Например, для поиска виноватого, если что-то пошло не так.

На следующем этапе QA-инженеры дополнительно проверяют, корректно ли работает код задачи совместно с другими задачами в билде. Если всё хорошо, тикету вручную выставляется статус In Build — OK.

On Production / On Production — OK / Closed


Далее на билде прогоняется весь наш набор тестов (Unit, интеграционные, Selenium- и т. д.). Если всё хорошо, билд мёржится в master, а код выкладывается на продакшен. Тикет переводится в статус On Production.

Далее разработчик (или заказчик) убеждается, что на продакшене фича работает корректно, и выставляет тикету статус On Production — OK.

Спустя две недели тикеты в статусе On Production — OK автоматически переводятся в статус Closed, если кто-то ранее не сделал это вручную.

Также стоит упомянуть дополнительные статусы, в которых может находится тикет:

  • Requirements — когда не получается оперативно получить от заказчика необходимые уточнения по задаче, а без них дальнейшая работа по тикету невозможна, тикет переводится в этот статус и назначается тому, кто должен дать разъяснения;
  • Suspended — если работа по тикету приостановлена, например, если разработчик заблокирован задачами смежной команды или был вынужден переключиться на более срочную задачу;
  • Reopened — задача может быть переоткрыта на разработчика после ревью, после тестирования, после неудачной попытки слияния ветки с master.


В результате упрощённая схема нашего workflow выглядит так:

-mcduen0-daw39ykqjhvp4rvdka.png


В результате прохождения тикета по флоу его шапка приобретает примерно такой вид:

c74tqp2l7smg9unpuxpgzw3fhxo.png

Что здесь ещё интересного, что мы настроили под себя и о чём я ещё не упомянул?


Обсуждение спорных моментов с постановщиком задачи мы стараемся вести в комментариях тикета, а не «размазывать» важные уточнения по почте и мессенджерам. Если же обсуждение всё же состоялось «на стороне», крайне желательно скопировать в тикет то, о чём договорились.

Помимо «человеческих» текстов, как я уже упоминал выше, в комментарии много всего пишется автоматически с помощью API:

  • коммиты;
  • результаты ревью;
  • результаты прогона тестов.


Иногда автоматические комментарии могут мешать, например, продакт-менеджерам. Поэтому мы сделали простенький JS-скрипт, который добавляет кнопку в интерфейс Jira и позволяет сворачивать все автоматические комментарии, оставляя только «человеческие». В итоге свёрнутые автоматические комментарии выглядят компактно.

nx_-jh1r3hxzby1j1ue7itohe0e.png

JS-код скрипта, который мы встроили в шаблон тикета
window.addEventListener('load', () => {

    const $ = window.jQuery;

    const botsAttrMatch = [
        'aida',
        'itops.api'
    ].map(bot => `[rel="${bot}"]`).join(',');

    if (!$) {
        return;
    }

    const AIDA_COLLAPSE_KEY = 'aida-collapsed';
    const COMMENT_SELECTOR = '.issue-data-block.activity-comment.twixi-block';

    const JiraImprovements = {
        init() {
            this.addButtons();
            this.handleAidaCollapsing();
            this.handleCommentExpansion();

            // Handle toggle button and aida collapsing and put it on a loop
            // to handle unexpected JIRA behaviour
            const self = this;
            setInterval(function () {
                self.addButtons();
                self.handleAidaCollapsing();
            }, 2000);

            addCss(`
                #badoo-toggle-bots {
                    background: #fff2c9;
                    color: #594300;
                    border-radius: 0 3px 0 0;
                    margin-top: 3px;
                    display: inline-block;
                }
            `);
        },

        addButtons() {
            // Do we already have the button?
            if ($('#badoo-toggle-bots').length > 0) {
                return;
            }

            // const headerOps = $('ul#opsbar-opsbar-operations');
            const jiraHeader = $('#issue-tabs');

            // Only add it in ticket state
            if (jiraHeader.length > 0) {
                const li = $('Collapse Bots');

                li.on('click', this.toggleAidaCollapsing.bind(this));

                jiraHeader.append(li);
            }
        },

        toggleAidaCollapsing(e) {
            e.preventDefault();

            const isCollapsed = localStorage.getItem(AIDA_COLLAPSE_KEY) === 'true';

            localStorage.setItem(AIDA_COLLAPSE_KEY, !isCollapsed);
            this.handleAidaCollapsing();
        },

        handleAidaCollapsing() {
            const isCollapsed = localStorage.getItem(AIDA_COLLAPSE_KEY) === 'true';
            const aidaComments = $(COMMENT_SELECTOR).has(botsAttrMatch).not('.manual-toggle');

            if (isCollapsed) {
                aidaComments.removeClass('expanded').addClass('collapsed');
                $('#badoo-toggle-bots').text('Show Bots');
            }
            else {
                aidaComments.removeClass('collapsed').addClass('expanded');
                $('#badoo-toggle-bots').text('Collapse Bots');
            }
        },

        handleCommentExpansion() {
            $(document.body).delegate('a.collapsed-comments', 'click', function () {

                const self = this; // eslint-disable-line no-invalid-this
                let triesLeft = 100;

                const interval = setInterval(() => {
                    if (--triesLeft < 0 || self.offsetHeight === 0) {
                        clearInterval(interval);
                    }

                    // Element has been removed from DOM. i.e. new jira comments have been added
                    if (self.offsetHeight === 0) {
                        JiraImprovements.handleAidaCollapsing();
                    }
                }, 100);
            });

            $(document.body).delegate(COMMENT_SELECTOR, 'click', function () {
                $(this).addClass('manual-toggle');// eslint-disable-line no-invalid-this
            });
        }
    };

    JiraImprovements.init();

    function addCss(cssText) {
        const style = document.createElement('style');

        style.type = 'text/css';
        if (style.styleSheet) {
            style.styleSheet.cssText = cssText;
        }
        else {
            style.appendChild(document.createTextNode(cssText));
        }

        document.head.appendChild(style);
    }

});



Ещё с помощью API и webhooks Jira мы делаем такие вещи:

  • отправляем уведомление в HipChat, если в комментарии был упомянут кто-то из сотрудников (очень способствует оперативному решению вопросов);
  • отправляем уведомления в HipChat при назначении тикета на ревью и когда тикет попадает на продакшен (как именно мы это реализовали, расскажу в следующей статье);
  • системные архитекторы с помощью специального интерфейса в пару кликов создают тикеты различным командам (клиентским и серверным) для реализации проекта (при этом тикеты корректно заполняются нужными полями и линкуются между собой; это помогает нам эффективно организовать синхронизацию работы команд);
  • мы автоматически отслеживаем появление новых версий клиентов; после этого специальный скрипт создаёт тикет серверной команде, чтобы мы внесли изменения в некоторые конфиги;
  • скрипт периодически снимает срезы по задачам в статусе In progress для статистики;
  • скрипт определяет задачи, которые надолго «зависают» в определённых статусах (например, On Review), и отправляет соответствующие уведомления ответственным сотрудникам;
  • если сотрудник в этот день отсутствует в офисе и об этом есть соответствующая запись во внутренней базе, то к его имени в Jira добавляется информация об этом (например, «d.semenihin (Day off)»). Очень полезная фича.


Jira — прекрасный инструмент, который в стандартной поставке позволяет решать большинство проблем, связанных с организацией ведения проектов. Но, как известно, в любом бизнесе есть свои нюансы. И для адаптации Jira к особенностям ваших процессов этот продукт обладает дополнительными возможностями, которые в умелых руках позволят вам решить практически любую проблему.

В следующей статье я планирую поделиться нашим опытом по настройке дашбордов — лидских и девелоперских. Также расскажу про настройку уведомлений в Jira и поделюсь секретами о том, как мы организуем синхронизацию работы разных команд на базе Jira. Надеюсь, что-то из нашего опыта пригодится и вам.

Спасибо за внимание!

© Habrahabr.ru