Самотестируемая система с оповещениями на Laravel + Bitbucket + HipChat

в 14:58, , рубрики: bitbucket, ci, curl, Git, hipchat, laravel, php

В этой статье я расскажу, как можно оперативно настроить автоматическое стягивание нового кода на тестовый сервер вашего laravel-приложения, автозапуск тестов и оповещение о результате в соответствующий корпоративный чат. А также отлавливание новых ошибок в laravel.log

Привет, меня зовут Дмитрий Корец, и я PHP разработчик в небольшой продуктовой компании.
У нас в качестве хостинга кода используется Bitbucket, общение команды через HipChat, поэтому и работать будет с ними.

Как мы знаем, одним из основых требований к реализации непрерывной интеграции является самотестируемость системы. И когда приложение не слишком большое, его не разрабатывают несколько независимых друг от друга команд, настройка различных Bamboo и Jenkins-ов кажется довольно затратной.

Итак, что мы хотим?

При пуше в репозиторий должно происходить следующее:

  • Оповещение в HipChat комнату о изменениях в коде
  • Новый коммит стягивается на сервере
  • Запуск тестов на сервере
  • В случае успеха отправляем сообщение об успехе, в случае ошибок — краткий stacktrace

Более того:

  • Отлавливание ошибок в laravel.log с последующим оповещением в корпоративный чат
  • Парсинг строки коммита для дополнительных манипуляций на сервере
  • Анализ ветки, в которую был сделан коммит

Начнем!

В битбакета, как и в гитхаба есть так называемые вебхуки(webhooks). Это подразумевает отправку HTTP запроса с определенными данными в JSON формате на указанный вами URL при определенных изменениях в репозитории.

Идем в Settings -> Webhooks -> Add Webhook

image

Как видим, списов довольно большой. Нас пока интересует только Push(при слиянии веток будет также срабатывать событие push), указываем нужный нам url, сохраняем — готово!

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

Самотестируемая система с оповещениями на Laravel + Bitbucket + HipChat - 2

Перейдем во 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;
    }
}

Пример того, что должны получить:

Самотестируемая система с оповещениями на Laravel + Bitbucket + HipChat - 3

Ну и, напоследок, добавим ещё одно событие.

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 будем отправляться сообщение к нам в комнату. Этот код необходимо поместить в одном из ваших сервис провайдеров. Напомню, есть такие типы сообщений в логах:

  1. debug
  2. info
  3. notice
  4. warning
  5. error
  6. critical
  7. alert
  8. emergency

Заключение

На этом у меня всё. Прошу простить за возможную плохую струтурированность информации, мой первый пост. Код доступен здесь. В виде отдельного composer пакета не оформлял, лень взяла надо мной верх. Также, можно добавить дополнительную логику. Например, если у вас активно используются продукты Atlassian, помимо битбакета и хипчата есть ещё и джира, то можно добавить возможность автоматического закрытия задачи в джире и перевода её на просмотр тестировщикам, если текст коммита содержит код таски в джире. Или, если проекту одного git pull уже недостаточно, нужно публиковать конфиги, перестраивать базу, заполнять начальными данными и т.д., то можно написать баш скрипт деплоя проекта и запускать его на сервере, если текст коммита содержит определенную подстроку.

Спасибо за внимание!

Автор: Дмитрий Корец

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js