В этой статье я расскажу, как можно оперативно настроить автоматическое стягивание нового кода на тестовый сервер вашего laravel-приложения, автозапуск тестов и оповещение о результате в соответствующий корпоративный чат. А также отлавливание новых ошибок в laravel.log
Привет, меня зовут Дмитрий Корец, и я PHP разработчик в небольшой продуктовой компании.
У нас в качестве хостинга кода используется Bitbucket, общение команды через HipChat, поэтому и работать будет с ними.
Как мы знаем, одним из основых требований к реализации непрерывной интеграции является самотестируемость системы. И когда приложение не слишком большое, его не разрабатывают несколько независимых друг от друга команд, настройка различных Bamboo и Jenkins-ов кажется довольно затратной.
Итак, что мы хотим?
При пуше в репозиторий должно происходить следующее:
Более того:
Начнем!
В битбакета, как и в гитхаба есть так называемые вебхуки(webhooks). Это подразумевает отправку HTTP запроса с определенными данными в JSON формате на указанный вами URL при определенных изменениях в репозитории.
Идем в Settings -> Webhooks -> Add Webhook

Как видим, списов довольно большой. Нас пока интересует только Push(при слиянии веток будет также срабатывать событие push), указываем нужный нам url, сохраняем — готово!
Теперь при пуше в репозиторий будет отправляться указанный выше запрос. Даже если мы ещё не настроили нужные роуты в нашем приложении, мы можем увидеть всё с самого битбакета: Settings -> Webhooks -> View requests напротив созданного нами хука.

Перейдем во View details и увидим, собственно, сам джсон обьект, который был отправлен, ответ от нашего сервера, статус код, время ответа и другое.
В хипчата есть встроенная интеграция с битбакетом. Но всё, что там доступно — подключения уведомлений об изменениях в репозитории. Передо мной стояла задача получать уведомления только при обновлении определенной ветки, поскольку в моей компании ядро продукта постоянно дорабатывается, паралельно с этим на уже существующем ядре релизятся новые проекты.
Поэтому, на этапе разработки у нас такая структура веток в репозитории с ядром(условно):
Саму комнату, я думаю, сами знаете как создать, далее переходим в неё и жмем Add integration -> Build your own integration, придумываем название, от имени этой интеграции будут приходить сообщения в чат. Подтверждаем. На следующей странице получаем самое важное — урл с токеном и сразу же пример curl команды для теста.
Создаем роут:
Контроллер:
Давайте разберемся. Здесь я сразу немного раскидал код по сервисам, дабы не помещать море логики в контроллер. Основная часть действий прописана в RuntTestsInQueueAndNotifyHipChatRoom.
Содержание RuntTestsInQueueAndNotifyHipChatRoom:
Как мы собираемся запускать команды на сервере в командой строке? В Laravel'e используется компонент Symfony Process(документация). Важный момент, от имени какого пользователя будут запускаться команды, учтите это!
Далее код с комментариями наших сервисов.
Стягиваем изменения с битбакета:
Запускаем тесты:
Отправка нотификаций сделана обычным curl запросом, к массиву данных применяем json_encode()
Пример того, что должны получить:

Ну и, напоследок, добавим ещё одно событие.
Теперь каждый раз при добавлении в laravel.log сообщения с типом CRITICAL будем отправляться сообщение к нам в комнату. Этот код необходимо поместить в одном из ваших сервис провайдеров. Напомню, есть такие типы сообщений в логах:
На этом у меня всё. Прошу простить за возможную плохую струтурированность информации, мой первый пост. Код доступен здесь. В виде отдельного composer пакета не оформлял, лень взяла надо мной верх. Также, можно добавить дополнительную логику. Например, если у вас активно используются продукты Atlassian, помимо битбакета и хипчата есть ещё и джира, то можно добавить возможность автоматического закрытия задачи в джире и перевода её на просмотр тестировщикам, если текст коммита содержит код таски в джире. Или, если проекту одного git pull уже недостаточно, нужно публиковать конфиги, перестраивать базу, заполнять начальными данными и т.д., то можно написать баш скрипт деплоя проекта и запускать его на сервере, если текст коммита содержит определенную подстроку.
Спасибо за внимание!
Привет, меня зовут Дмитрий Корец, и я 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 уже недостаточно, нужно публиковать конфиги, перестраивать базу, заполнять начальными данными и т.д., то можно написать баш скрипт деплоя проекта и запускать его на сервере, если текст коммита содержит определенную подстроку.
Спасибо за внимание!
