Если вы работаете в IT-компании, то, скорее всего, ваши процессы построены вокруг известного продукта Atlassian — Jira. На рынке есть множество таск-трекеров для решения тех же задач, в том числе open-source-решения (Trac, Redmine, Bugzilla), но, пожалуй, именно Jira имеет сегодня самое широкое распространение.
Меня зовут Дмитрий Семенихин, я тимлид в компании Badoo. В небольшом цикле статей я расскажу, как именно мы используем Jira, как настраивали её под свои процессы, что хорошего «прикрутили» сверху и как тем самым превратили issue-трекер в единый центр коммуникаций по задаче и упростили себе жизнь. В этой статье вы увидите наш флоу изнутри, узнаете, как можно «докрутить» свою Jira, и прочтёте о дополнительных возможностях инструмента, о которых могли не знать.
Статья ориентирована прежде всего на тех, кто уже использует 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, который подготавливает комментарий к коммиту, получая номер тикета из имени текущей ветки:
Если коммит с «неправильным» комментарием попытаться запушить, такой пуш будет отклонён. Также отклонена будет попытка запушить ветку без номера тикета в начале.
Эти требования контролируются 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. Выглядит это так:
(branchdiff — это ссылка на diff ветки с головой, от которой взяла своё начало текущая ветка, в нашем инструменте ревью кода Codeisok);
- создаётся комментарий в тикете со всеми коммитами в данном пуше.
(Aida — это условное название нашего комплекса автоматизации для работы с Jira, Git и не только. Именно от этого имени появляются автоматические комментарии в тикете. Подробнее об Aida мы писали в статье).
Клик по хешу коммита открывает diff с предыдущей ревизией ветки (как это примерно выглядит, покажу ниже);
- проверяется, есть ли в ветке файлы, для которых может потребоваться перевод на поддерживаемые языки (например, шаблоны web-страниц), и если такие есть, то кастомному полю тикета Lexems проставляется значение New\Changed. Это гарантирует, что тикет не уедет на продакшен без законченного перевода;
- в список девелоперов (кастомное поле тикета Developers) добавляется имя сотрудника, который пушит ветку.
On Review
Написав код и самостоятельно убедившись, что все требования к задаче выполнены, а тесты не сломаны, разработчик назначает тикет ревьюверу (статус On Review). Обычно разработчик сам решает, кто будет ревьювить его тикет. Скорее всего, это будет другой разработчик, который отлично разбирается в нужной части кода. Ревью происходит с помощью инструмента Codeisok, который открывается сразу с нужным diff по клику на ссылку branchdiff в поле тикета Commits или на ссылку в виде хеша коммита в комментариях.
Ревьювер видит примерно такую картину:
Закончив ревью, ревьювер нажимает кнопку Finish, и, помимо всего прочего, в этот момент происходит следующее:
- с помощью API JIra создаётся комментарий в тикете с замечаниями ревьювера в контексте кода. Выглядит это примерно так:
- если замечания к коду были и ревьювер решил переоткрыть тикет, то разработчику придёт уведомление об этом в HipChat (это делается с помощью правила webhook, которое срабатывает на переоткрытие);
- заполняется поле тикета Reviewers.
Resolved
Далее, если ревью прошло успешно, тикет отправляется в беклог QA-инженеров в статусе Resolved. Но вместе с этим с помощью webhook на событие resolved в фоне запускаются автоматические тесты на коде ветки. Спустя несколько минут в тикете появится новый комментарий, который сообщит о результатах тестов.
Также в любой момент можно вручную инициировать повторный прогон тестов, кликнув по специальной кнопке Run unit tests в меню тикета. После успешного прогона в тикете появится новый комментарий, аналогичный предыдущему.
По сути, эта кнопка — один из дополнительных статусов задачи в workflow Jira, перевод в который инициирует срабатывание скрипта на Groovy для плагина ScriptRunner. Скрипт вызывает внешний URL, который инициирует прогон тестов, и если URL ответил успехом, то тикет возвращается в предыдущий статус (в нашем случае Resolved).
In Shot / In Shot — OK
Задача сначала тестируется в devel-окружении. Если всё хорошо, создаётся шот (например, кликом по ссылке Create shot в поле Commits) — директория на выделенном сервере, в которую копируются изменения из тикета, смёрженные с текущим master. Сервер работает с продакшен-данными: базы и сервисы те же, что обслуживают реальных пользователей. Таким образом, тестировщик может открыть web-сайт или подключиться к шоту с помощью мобильного клиента и «изолированно» проверить фичу в продакшен-окружении. «Изолированно» значит, что никакой другой код/функционал, кроме нового из ветки и текущего master, не исполняется. Поэтому этот этап тестирования является, пожалуй, основным, так как позволяет QA-инженеру максимально достоверно найти проблему непосредственно в тестируемой задаче.
Доступ к ресурсам шота осуществляется по специальным URL, которые генерируются в скрипте создания шота и с помощью API Jira помещаются в шапку тикета. В результате мы видим ссылки на сайт, админку, логи и прочие инструменты, которые исполняются в шот-окружении:
Также в момент генерации шота запускается скрипт, который анализирует содержимое изменённых файлов и создаёт заявки на перевод найденных новых лексем. После того как перевод закончен, значение поля 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 пишется название билда.
Далее, используя это значение, легко получить список тикетов, которые были выложены с каждым билдом. Например, для поиска виноватого, если что-то пошло не так.
На следующем этапе 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 выглядит так:
Тикет — центр коммуникаций по задаче
В результате прохождения тикета по флоу его шапка приобретает примерно такой вид:
Что здесь ещё интересного, что мы настроили под себя и о чём я ещё не упомянул?
- Component — используется для кластеризации тикета в рамках большого отдела. Разные подгруппы отвечают за разные компоненты и, соответственно, на своих дашбордах видят только задачи по своим компонентам. Например, я могу вывести список всех открытых багов по компонентам моей команды таким запросом:
Project = SRV AND type = Bug AND status = Open AND component in componentsLeadByUser(d.semenihin)
- Review — нужно ли ревью кода. По умолчанию нужно. Если значение поля установлено в No, тикет сразу попадёт в статус Resolved.
QA — нужна ли проверка тестировщиком. По умолчанию нужна. Если значение поля установлено в No, тикет сразу попадёт в статус In Shot — OK.
Sprint — в нашем случае актуально только для задач с типом New Feature, план для которых мы составляем заранее на неделю.
- Due date — определение разработчиком даты, когда тикет будет на продакшене. Выставляется перед началом работы над задачей.
- Situation — по сути, краткий лог с кратким описанием текущего статуса по задаче. Например, «20/08 Жду переводов», «21/08 Требуется уточнение от заказчика по проблеме Х». Это помогает видеть краткую сводку по задаче в списке других задач.
- Msg4QA — информация для QA-инженеров, которой делится разработчик, чтобы упростить процесс тестирования
Обсуждение спорных моментов с постановщиком задачи мы стараемся вести в комментариях тикета, а не «размазывать» важные уточнения по почте и мессенджерам. Если же обсуждение всё же состоялось «на стороне», крайне желательно скопировать в тикет то, о чём договорились.
Помимо «человеческих» текстов, как я уже упоминал выше, в комментарии много всего пишется автоматически с помощью API:
- коммиты;
- результаты ревью;
- результаты прогона тестов.
Иногда автоматические комментарии могут мешать, например, продакт-менеджерам. Поэтому мы сделали простенький JS-скрипт, который добавляет кнопку в интерфейс Jira и позволяет сворачивать все автоматические комментарии, оставляя только «человеческие». В итоге свёрнутые автоматические комментарии выглядят компактно.
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 = $('<a id="badoo-toggle-bots" class="aui-button aui-button-primary aui-style" href="/">Collapse Bots</a>');
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. Надеюсь, что-то из нашего опыта пригодится и вам.
Спасибо за внимание!