Привет, меня зовут Дмитрий Корец, и я PHP разработчик в небольшой продуктовой компании.
У нас в качестве хостинга кода используется Bitbucket, общение команды через HipChat, поэтому и работать будет с ними.
Как мы знаем, одним из основых требований к реализации непрерывной интеграции является самотестируемость системы. И когда приложение не слишком большое, его не разрабатывают несколько независимых друг от друга команд, настройка различных Bamboo и Jenkins-ов кажется довольно затратной.
Итак, что мы хотим?
При пуше в репозиторий должно происходить следующее:
- Оповещение в HipChat комнату о изменениях в коде
- Новый коммит стягивается на сервере
- Запуск тестов на сервере
- В случае успеха отправляем сообщение об успехе, в случае ошибок — краткий stacktrace
Более того:
- Отлавливание ошибок в laravel.log с последующим оповещением в корпоративный чат
- Парсинг строки коммита для дополнительных манипуляций на сервере
- Анализ ветки, в которую был сделан коммит
Начнем!
В битбакета, как и в гитхаба есть так называемые вебхуки(webhooks). Это подразумевает отправку HTTP запроса с определенными данными в JSON формате на указанный вами URL при определенных изменениях в репозитории.
Идем в Settings -> Webhooks -> Add Webhook
Как видим, списов довольно большой. Нас пока интересует только Push(при слиянии веток будет также срабатывать событие push), указываем нужный нам url, сохраняем — готово!
Теперь при пуше в репозиторий будет отправляться указанный выше запрос. Даже если мы ещё не настроили нужные роуты в нашем приложении, мы можем увидеть всё с самого битбакета: Settings -> Webhooks -> View requests напротив созданного нами хука.
Перейдем во View details и увидим, собственно, сам джсон обьект, который был отправлен, ответ от нашего сервера, статус код, время ответа и другое.
Добавляем интеграцию в HipChat комнату
В хипчата есть встроенная интеграция с битбакетом. Но всё, что там доступно — подключения уведомлений об изменениях в репозитории. Передо мной стояла задача получать уведомления только при обновлении определенной ветки, поскольку в моей компании ядро продукта постоянно дорабатывается, паралельно с этим на уже существующем ядре релизятся новые проекты.
Поэтому, на этапе разработки у нас такая структура веток в репозитории с ядром(условно):
- staging
- master
- master_project1
- master_project1
Саму комнату, я думаю, сами знаете как создать, далее переходим в неё и жмем Add integration -> Build your own integration, придумываем название, от имени этой интеграции будут приходить сообщения в чат. Подтверждаем. На следующей странице получаем самое важное — урл с токеном и сразу же пример curl команды для теста.
curl -d '{"color":"green","message":"My first notification (yey)","notify":false,"message_format":"text"}' -H 'Content-Type: application/json' https://youcompany.hipchat.com/v2/room/{roomId}/notification?auth_token={token}
Хук настроен, HipChat комната создана, пишем логику
Создаем роут:
<?php
Route::group(['prefix' => LaravelLocalization::setLocale()],
function () {
Route::group(
[
'prefix' => 'development',
],
function () {
Route::group(['prefix' => 'bitbucket',
'namespace' => '\BE\Dev\Backend\Http\Controllers'], function () {
Route::post('/', [
'as' => 'bitbucket.push_event',
'uses' => 'BitbucketEventsController@pushEvent'
]);
});
}
);
}
);
Контроллер:
<?php
namespace BE\Dev\Backend\Http\Controllers;
use App\Http\Controllers\Controller;
use BE\Dev\Services\Bitbucket\BitbucketPushEventService;
use BE\Dev\Services\HipChat\HipChatService;
use Illuminate\Http\Request;
class BitbucketEventsController extends Controller
{
<?php
namespace BE\Dev\Backend\Http\Controllers;
use App\Http\Controllers\Controller;
use BE\Dev\Services\Bitbucket\BitbucketPushEventService;
use BE\Dev\Services\HipChat\HipChatService;
use Illuminate\Http\Request;
class BitbucketEventsController extends Controller
{
/**
* @var $hipchatService HipChatService
*/
protected $hipchatService;
/**
* @var $pushService BitbucketPushEventService
*/
protected $pushService;
protected $config;
public function __construct(Request $request)
{
$this->config = config('be_dev');
$this->hipchatService = app(HipChatService::class);
$this->pushService = app(BitbucketPushEventService::class, [$request->all()]);
}
public function pushEvent(Request $request)
{
$data = $request->all();
// если коммит не из ветки staging - выходим из метода
if ($this->pushService->getBranch() != 'staging') {
return false;
}
// получение комментария
$comment = strtolower($data['push']['changes'][0]['commits'][0]['message']);
// Получение автора коммита
$author = $data['actor']['display_name'];
$data = [
'color' => 'green',
'message' => "<strong>{$author}</strong> только что запушил в репозиторий BE с комментарием \"{$comment}\"",
'notify' => true,
'message_format' => 'html',
];
// отправляем уведомление
$this->hipchatService->sendNotification($data);
// Если комментарий коммита содержит подстроку no tests - выходим из метода
if (strpos($comment, 'no tests')) {
return response()->json([
'success' => true,
'message' => 'tests was not executed'
])->setStatusCode(200);
}
// git pull + запуск тестов + оповщение в комнату
$service = new \BE\Dev\Services\RuntTestsInQueueAndNotifyHipChatRoom();
$service->handle();
return response()->json([
'success' => true
])->setStatusCode(200);
}
}
Давайте разберемся. Здесь я сразу немного раскидал код по сервисам, дабы не помещать море логики в контроллер. Основная часть действий прописана в RuntTestsInQueueAndNotifyHipChatRoom.
Содержание RuntTestsInQueueAndNotifyHipChatRoom:
<?php
namespace BE\Dev\Services;
use BE\Dev\Services\HipChat\HipChatService;
use Illuminate\Bus\Queueable;
use BE\Jobs\Job;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class RuntTestsInQueueAndNotifyHipChatRoom extends Job implements ShouldQueue
{
use Queueable, SerializesModels;
/**
* @var $deployService DeployService
*/
protected $deployService;
/**
* @var $tests RunTests
*/
protected $tests;
/**
* @var $hipchatService HipChatService
*/
protected $hipchatService;
public function __construct()
{
$this->deployService = app(DeployService::class);
$this->tests = app(RunTests::class);
$this->tests = app(HipChatService::class);
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
try {
// git pull на сервере
$this->deployService->pullPackagesChanges();
// запуск тестов
$outputTests = $this->tests->run();
if ($outputTests === true) {
$outputTests = 'Тесты прошли успешно';
$colorTests = 'green';
} else {
$colorTests = 'red';
}
// после тестов отправляем сообщение об их успешном прохождении или фейле
$this->hipchatService->sendNotification([
'color' => $colorTests,
'message' => $outputTests,
'notify' => true,
'message_format' => 'html',
]);
} catch (\Exception $exception) {
\Log::error($exception->getMessage());
}
}
}
Как мы собираемся запускать команды на сервере в командой строке? В Laravel'e используется компонент Symfony Process(документация). Важный момент, от имени какого пользователя будут запускаться команды, учтите это!
Далее код с комментариями наших сервисов.
Стягиваем изменения с битбакета:
<?php
namespace BE\Dev\Services;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class DeployService
{
public function pullPackagesChanges()
{
// cd ../ необходим, поскольку наша точка входа index.php находится в папке public,
// а нам нужно выйти в корень проекта
$process = new Process('cd ../ && git pull');
$process->run();
// executes after the command finishes
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process->getOutput();
}
}
Запускаем тесты:
<?php
namespace BE\Dev\Services;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class RunTests
{
/**
* @return string
*/
public function run()
{
$process = new Process('cd ../ && vendor/bin/phpunit --tap');
$process->run();
// executes after the command finishes
if (!$process->isSuccessful()) {
return $process->getIncrementalOutput();
}
return true;
}
}
Отправка нотификаций сделана обычным curl запросом, к массиву данных применяем json_encode()
<?php
namespace BE\Dev\Services\HipChat;
class HipChatService
{
/**
* @var $config array Service config
*/
protected $config;
public function __construct()
{
$this->config = config('be_dev.hipchat');
}
public function sendNotification($data)
{
// create curl resource
$ch = curl_init();
// set url
curl_setopt($ch, CURLOPT_URL, $this->config['url']);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
//return the transfer as a string
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// $output contains the output string
$output = curl_exec($ch);
// close curl resource to free up system resources
curl_close($ch);
return $output;
}
}
Пример того, что должны получить:
Ну и, напоследок, добавим ещё одно событие.
Log::getMonoLog()->pushHandler(new \Monolog\Handler\HipChatHandler(
’AUTH_TOKEN’, ‘ROOM_ID', ‘hipchat-app’, true, \Monolog\Logger::CRITICAL, true, true, ‘text', 'COMPANY_NAME'.hipchat.com, 'v2'
));
Теперь каждый раз при добавлении в laravel.log сообщения с типом CRITICAL будем отправляться сообщение к нам в комнату. Этот код необходимо поместить в одном из ваших сервис провайдеров. Напомню, есть такие типы сообщений в логах:
- debug
- info
- notice
- warning
- error
- critical
- alert
- emergency
Заключение
На этом у меня всё. Прошу простить за возможную плохую струтурированность информации, мой первый пост. Код доступен здесь. В виде отдельного composer пакета не оформлял, лень взяла надо мной верх. Также, можно добавить дополнительную логику. Например, если у вас активно используются продукты Atlassian, помимо битбакета и хипчата есть ещё и джира, то можно добавить возможность автоматического закрытия задачи в джире и перевода её на просмотр тестировщикам, если текст коммита содержит код таски в джире. Или, если проекту одного git pull уже недостаточно, нужно публиковать конфиги, перестраивать базу, заполнять начальными данными и т.д., то можно написать баш скрипт деплоя проекта и запускать его на сервере, если текст коммита содержит определенную подстроку.
Спасибо за внимание!
Комментарии (9)
lxsmkv
06.11.2017 02:50прочитав слово «самотестируемая» подумал про что-то из разряда model based testing. A на самом деле это просто DIY система CI. Тесты тут не генерируются сами. Но как инструкция для начинающих девопсов — полезно.
antondukhanin
06.11.2017 11:37Интересно, но есть подозрение, что большую часть логики (в частности сработка при пуше на определенную ветку) можно было перенести в piplines гораздо меньшей кровью (с меньшим количеством кода и контроллеров, буквально в 10 строк). В gitlab-ci это точно есть, стоит думать что и у bitbucket в его pipelines. Логика интеграции лежала бы отдельно от логики приложения.
Что касается интеграции с чатом — думаю, тоже много готовых механизмов. А если нет — то можно было бы так же накрутить HipChatService в laravel в тестах.vtvz_ru
06.11.2017 13:35Pipelines условно платная услуга. 50 минут в месяц может не хватить. Из этих соображений некоторые пилят свои велосипеды (как я и автор статьи).
Гитлаб хоть и бесплатный, но для него лучше купить второй сервер, чтобы сильно основной не нагружать. А, откровенно говоря, не все могут позволить себе иметь два сервера (как я и, возможно, автор статьи)
Но Вы абсолютно правы. Специализированные, изначально интегрированные и проверенные временем и тысячами разработчиками инструменты намного лучше и надежнее, чем подобные решения.
Borro
06.11.2017 14:47Я для хипчата использовал либу https://github.com/gorkalaucirica/HipchatAPIv2Client. Очень удобная.
vtvz_ru
Раньше тоже пользовался HipChat. Но столкнулся с проблемой, что на мобилках приложение не особо держится и вычищается. Из-за чего все уведомления пролетали мимо. Мы себе этого позволить не могли, так как в таких сообщениях могла содержаться очень важная на текущий момент информация. Что я только не делал (вроде там галочка была, чтобы он не терял соединение при сворачивании, но проблемы она не решала; это было давно, возможно, сейчас проблема исправлена). В итоге перешли в Slack. С тех пор таких проблем не наблюдалось.
Говоря про Bitbucket веб-хуки. Это хорошо, чтобы только уведомить сервер о том, что произошел пуш. И не более. У меня тоже происходила сборка по хуку, но продолжительность сборки была дольше, чем Bitbucket позволял мне, поэтому происходил timeout и еще две попытки, что запукало 3 сборки почти параллельно. Сейчас сделано так: веб-хук только записывает в файл, что произошло изменение (в моем случае пишет еще и в какую ветку, чтобы можно было собрать dev и release отдельно; у меня информация хранится в JSON массиве, чтобы если я запушил сразу в две ветки, прошло две независимых сборки). А по крону запускается ежеминутно скрипт, который и запускает сборку.
Сейчас в планах запуск тестов и сборку проекта перенести в Pipelines Это почти как CI, но попроще и интегрировано прямо в bitbucket. Там 50 минут в месяц бесплатно; для малых проектов хватит. Если хочется больше, то можно докупить 1000 минут за $10.
Ticman Автор
Да, битбакет выставляет таймаут запросу в 10 секунд. Когда порядка тысячи тестов в приложении, разумеется, тесты не успеют выполниться за такое время. Но на то у нас есть очереди, которые в laravel'е настриваются довольно легко. Забыл об этом упомянуть в статье, спасибо!
Я думал и даже пробывал настроить пайплайны, но, как вы сами сказали, бесплатное время билда — 50 минут. А билдить проект при каждом коммите, по крайней мере, у меня, в итоге займет на порядок больше времени в конце месяца.
vtvz_ru
Там есть кастомные настройки, которые можно запустить вручную. Там в этом плане все достаточно гибко.
Насколько я знаю, запускается он только на последний коммит и только так, как настроишь. У нас, например, ветка master для разработки, которая ни на что не влияет. Там особо не нужно ничего запускать. А вот ветка release уже предназначена для кода в продакшн. Вот там вот уже и запускать тесты, сборку и прочее.
Я хочу сначала попробовать сделать так, чтобы все запускалось через pipeline, но работало как раньше, через веб-хуки. Если времени хватит, то перейдем в pipeline полностью. Или если начальство не по-жадничает $10 в месяц на дополнительные минуты)
Ticman Автор
Не знал про кастомные настройки пайплайнов, дякую.