- PVSM.RU - https://www.pvsm.ru -

Если вы работаете в IT-компании, то, скорее всего, ваши процессы построены вокруг известного продукта Atlassian — Jira. На рынке есть множество таск-трекеров для решения тех же задач, в том числе open-source-решения (Trac, Redmine, Bugzilla), но, пожалуй, именно Jira имеет сегодня самое широкое распространение.
Меня зовут Дмитрий Семенихин, я тимлид компании Badoo. В небольшом цикле статей я расскажу, как именно мы используем Jira, как настраивали её под свои процессы, что хорошего «прикрутили» сверху и как тем самым превратили issue-трекер в единый центр коммуникаций по задаче и упростили себе жизнь. В этой статье вы увидите наш флоу изнутри, узнаете, как можно «докрутить» свою Jira, и прочтёте о дополнительных возможностях инструмента, о которых могли не знать.
Статья ориентирована прежде всего на тех, кто уже использует Jira, но, возможно, испытывает сложности с интеграцией её стандартных возможностей в существующие в компании процессы. Также статья может быть полезна компаниям, которые используют другие таск-трекеры, но столкнулись с некоторыми ограничениями и подумывают о смене решения. Статья построена не по принципу «проблема — решение», в ней я описываю сложившийся инструментарий и фичи, построенные нами вокруг Jira, а также технологии, которые мы использовали для их реализации.
Чтобы последующий текст был более понятным, давайте разберёмся, какие инструменты предоставляет нам Jira для реализации нестандартных хотелок — тех, что выходят за рамки стандартного функционала Jira.
В общем случае вызов команды API — это HTTP-запрос к URL API с указанием метода (GET, PUT, POST and DELETE), команды и тела запроса. Тело запроса, а также ответ API — в JSON-формате. Пример запроса, который вернёт JSON-представление тикета:
GET /rest/api/latest/issue/{ticket_number}
С помощью API вы можете, используя скрипты на любом языке программирования:
Подробная документация об API представлена по ссылке [1].
Мы написали собственный высокоуровневый 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}");
}
С помощью webhook можно настроить вызов внешней callback-функции на вашем хосте на различные события в Jira. При этом можно настроить сколько угодно таких правил таким образом, что различные URL будут «дёргаться» для разных событий и для тикетов, которые соответствуют указанному в webhook фильтру. Интерфейс настройки webhooks доступен администратору Jira.
В результате можно создавать правила вроде этого:
Name: “SRV — New Feature created/updated”
URL: www.myremoteapp.com/webhookreceiver [2]
Scope: Project = SRV AND type in (‘New Feature’)
Events: Issue Updated, Issue Created
В данном примере указанный URL будет вызываться для событий создания и изменения тикетов, соответствующих фильтру Scope. При этом в теле запроса будет содержаться вся необходимая информация о том, что именно изменилось и какое событие произошло.
Тут важно понимать, что Jira не гарантирует, что ваше событие будет доставлено. Если внешний URL не ответил или ответил с ошибкой, это нигде видно не будет (кроме логов, пожалуй). Поэтому обработчик событий webhook должен быть максимально надёжным. Например, события можно складывать в очередь и пытаться обработать до тех пор, пока это не закончится успехом. Это поможет решить проблемы с временно недоступными сервисами, например, какой-либо внешней базой данных, необходимой для правильной обработки события.
Подробная документация о webhooks представлена по ссылке [3].
Это плагин к Jira, очень мощный инструмент, который позволяет кастомизировать в Jira очень многое (в том числе он способен заменить собой webhooks). Для пользования этим плагином требуется знание Groovy. Основное преимущество инструмента для нас состоит в том, что можно встраивать во флоу кастомную логику в режиме онлайн. Код вашего скрипта будет исполняться сразу в среде Jira в ответ на определённое действие. Например, можно сделать в интерфейсе тикета свою кнопку, клик по которой будет создавать связанные с текущей задачей тикеты или запускать юнит-тесты для данной задачи. И если вдруг что-то пойдёт не так, вы как пользователь сразу об этом узнаете.
Желающие могут ознакомиться с документацией [4].
А теперь о том, как мы применяем дополнительные возможности Jira в наших проектах. Рассмотрим это в контексте прохождения нашего типичного тикета по флоу от создания до закрытия. Заодно и про сам флоу расскажу.
Итак, сначала тикет попадает в беклог новых тикетов со статусом Open. Далее лид компонента, увидев новый тикет на своём дашборде, принимает решение: назначить тикет прямо сейчас разработчику либо отправить его в беклог известных тикетов (статус Backlog), чтобы назначить его позже, когда появится свободный разработчик и более приоритетные тикеты будут закрыты. Это может показаться странным, так как кажется логичным делать наоборот: создавать тикеты в статусе Backlog, а потом переводить в статус Open. Но у нас прижилась именно эта схема. Она позволяет легко настроить фильтры, чтобы сократить время принятия решения по новым тикетам. Пример JQL-фильтра, который показывает новые задачи лиду:
Project = SRV AND assignee is EMPTY AND status in (Open)
Эти требования контролируются 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. Специальный скрипт раз в два часа:
Сделав все необходимые коммиты, разработчик пушит ветку в общую репу. В этом случае срабатывает Git-хук post-receive, который делает много всего интересного:

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

(Aida — это условное название нашего комплекса автоматизации для работы с Jira, Git и не только. Именно от этого имени появляются автоматические комментарии в тикете. Подробнее об Aida мы писали в статье [7]).
Клик по хешу коммита открывает diff с предыдущей ревизией ветки (как это примерно выглядит, покажу ниже);
Написав код и самостоятельно убедившись, что все требования к задаче выполнены, а тесты не сломаны, разработчик назначает тикет ревьюверу (статус On Review). Обычно разработчик сам решает, кто будет ревьювить его тикет. Скорее всего, это будет другой разработчик, который отлично разбирается в нужной части кода. Ревью происходит с помощью инструмента Codeisok [6], который открывается сразу с нужным diff по клику на ссылку branchdiff в поле тикета Commits или на ссылку в виде хеша коммита в комментариях.
Ревьювер видит примерно такую картину:

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

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

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

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

Также в момент генерации шота запускается скрипт, который анализирует содержимое изменённых файлов и создаёт заявки на перевод найденных новых лексем. После того как перевод закончен, значение поля Lexems меняется на Done и тикет может быть добавлен в билд.
Если тестирование в шоте прошло успешно, то тикет переводится в статус In Shot — OK.
Мы выкладываем код два раза в день — утром и вечером. Для этого создаётся специальная build-ветка, которая в итоге будет слита с master и выложена «в бой».
В момент сборки build-ветки специальный скрипт с помощью JQL-запроса получает список тикетов в статусе In Shot — OK и пытается замёржить их в ветку билда при выполнении всех перечисленных ниже условий:
Стоит отметить, что автоматическое слияние может не произойти по причине конфликта слияния. В этом случае тикет автоматически переводится в статус Reopen и назначается разработчику, о чём он немедленно получает оповещение в HipChat, а в комментарий тикета добавляется соответствующее сообщение. После разрешения конфликта тикет возвращается в билд.
Если же всё хорошо и ветка тикета замёржилась в билд, тикет автоматически переводится в статус In Build, а в кастомное поле тикета Build_Name пишется название билда.

Далее, используя это значение, легко получить список тикетов, которые были выложены с каждым билдом. Например, для поиска виноватого, если что-то пошло не так.
На следующем этапе QA-инженеры дополнительно проверяют, корректно ли работает код задачи совместно с другими задачами в билде. Если всё хорошо, тикету вручную выставляется статус In Build — OK.
Далее на билде прогоняется весь наш набор тестов (Unit, интеграционные, Selenium- и т. д.). Если всё хорошо, билд мёржится в master, а код выкладывается на продакшен. Тикет переводится в статус On Production.
Далее разработчик (или заказчик) убеждается, что на продакшене фича работает корректно, и выставляет тикету статус On Production — OK.
Спустя две недели тикеты в статусе On Production — OK автоматически переводятся в статус Closed, если кто-то ранее не сделал это вручную.
Также стоит упомянуть дополнительные статусы, в которых может находится тикет:
В результате упрощённая схема нашего workflow выглядит так:

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

Что здесь ещё интересного, что мы настроили под себя и о чём я ещё не упомянул?
Project = SRV AND type = Bug AND status = Open AND component in componentsLeadByUser(d.semenihin)
Обсуждение спорных моментов с постановщиком задачи мы стараемся вести в комментариях тикета, а не «размазывать» важные уточнения по почте и мессенджерам. Если же обсуждение всё же состоялось «на стороне», крайне желательно скопировать в тикет то, о чём договорились.
Помимо «человеческих» текстов, как я уже упоминал выше, в комментарии много всего пишется автоматически с помощью API:
Иногда автоматические комментарии могут мешать, например, продакт-менеджерам. Поэтому мы сделали простенький JS-скрипт, который добавляет кнопку в интерфейс Jira и позволяет сворачивать все автоматические комментарии, оставляя только «человеческие». В итоге свёрнутые автоматические комментарии выглядят компактно.

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 мы делаем такие вещи:
Jira — прекрасный инструмент, который в стандартной поставке позволяет решать большинство проблем, связанных с организацией ведения проектов. Но, как известно, в любом бизнесе есть свои нюансы. И для адаптации Jira к особенностям ваших процессов этот продукт обладает дополнительными возможностями, которые в умелых руках позволят вам решить практически любую проблему.
В следующей статье я планирую поделиться нашим опытом по настройке дашбордов — лидских и девелоперских. Также расскажу про настройку уведомлений в Jira и поделюсь секретами о том, как мы организуем синхронизацию работы разных команд на базе Jira. Надеюсь, что-то из нашего опыта пригодится и вам.
Спасибо за внимание!
Автор: dsemenikhin
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/294579
Ссылки в тексте:
[1] ссылке: https://docs.atlassian.com/software/jira/docs/api/REST/7.12.0/
[2] www.myremoteapp.com/webhookreceiver: http://www.myremoteapp.com/webhookreceiver
[3] ссылке: https://developer.atlassian.com/server/jira/platform/webhooks/
[4] документацией: https://scriptrunner.adaptavist.com/latest/jira/quickstart.html
[5] статье: https://habr.com/company/badoo/blog/193258/
[6] Codeisok: https://habr.com/company/badoo/blog/354856/
[7] статье: https://habr.com/company/badoo/blog/169417/
[8] Источник: https://habr.com/post/424655/?utm_campaign=424655
Нажмите здесь для печати.