В этой статье я расскажу, как можно оперативно настроить автоматическое стягивание нового кода на тестовый сервер вашего laravel-приложения, автозапуск тестов и оповещение о результате в соответствующий корпоративный чат. А также отлавливание новых ошибок в laravel.log
Привет, меня зовут Дмитрий Корец, и я PHP разработчик в небольшой продуктовой компании.
У нас в качестве
Как мы знаем, одним из основых требований к реализации непрерывной интеграции является самотестируемость системы. И когда приложение не слишком большое, его не разрабатывают несколько независимых друг от друга команд, настройка различных 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' => 'BEDevBackendHttpControllers'], function () {
Route::post('/', [
'as' => 'bitbucket.push_event',
'uses' => 'BitbucketEventsController@pushEvent'
]);
});
}
);
}
);
Контроллер:
<?php
namespace BEDevBackendHttpControllers;
use AppHttpControllersController;
use BEDevServicesBitbucketBitbucketPushEventService;
use BEDevServicesHipChatHipChatService;
use IlluminateHttpRequest;
class BitbucketEventsController extends Controller
{
<?php
namespace BEDevBackendHttpControllers;
use AppHttpControllersController;
use BEDevServicesBitbucketBitbucketPushEventService;
use BEDevServicesHipChatHipChatService;
use IlluminateHttpRequest;
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 BEDevServicesRuntTestsInQueueAndNotifyHipChatRoom();
$service->handle();
return response()->json([
'success' => true
])->setStatusCode(200);
}
}
Давайте разберемся. Здесь я сразу немного раскидал код по сервисам, дабы не помещать море логики в контроллер. Основная часть действий прописана в RuntTestsInQueueAndNotifyHipChatRoom.
Содержание RuntTestsInQueueAndNotifyHipChatRoom:
<?php
namespace BEDevServices;
use BEDevServicesHipChatHipChatService;
use IlluminateBusQueueable;
use BEJobsJob;
use IlluminateQueueSerializesModels;
use IlluminateContractsQueueShouldQueue;
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 BEDevServices;
use SymfonyComponentProcessExceptionProcessFailedException;
use SymfonyComponentProcessProcess;
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 BEDevServices;
use SymfonyComponentProcessExceptionProcessFailedException;
use SymfonyComponentProcessProcess;
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 BEDevServicesHipChat;
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 MonologHandlerHipChatHandler(
’AUTH_TOKEN’, ‘ROOM_ID', ‘hipchat-app’, true, MonologLogger::CRITICAL, true, true, ‘text', 'COMPANY_NAME'.hipchat.com, 'v2'
));
Теперь каждый раз при добавлении в laravel.log сообщения с типом CRITICAL будем отправляться сообщение к нам в комнату. Этот код необходимо поместить в одном из ваших сервис провайдеров. Напомню, есть такие типы сообщений в логах:
- debug
- info
- notice
- warning
- error
- critical
- alert
- emergency
Заключение
На этом у меня всё. Прошу простить за возможную плохую струтурированность информации, мой первый пост. Код доступен здесь. В виде отдельного composer пакета не оформлял, лень взяла надо мной верх. Также, можно добавить дополнительную логику. Например, если у вас активно используются продукты Atlassian, помимо битбакета и хипчата есть ещё и джира, то можно добавить возможность автоматического закрытия задачи в джире и перевода её на просмотр тестировщикам, если текст коммита содержит код таски в джире. Или, если проекту одного git pull уже недостаточно, нужно публиковать конфиги, перестраивать базу, заполнять начальными данными и т.д., то можно написать баш скрипт деплоя проекта и запускать его на сервере, если текст коммита содержит определенную подстроку.
Спасибо за внимание!
Автор: Дмитрий Корец