В этой статье я расскажу, как можно оперативно настроить автоматическое стягивание нового кода на тестовый сервер вашего 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 напротив созданного нами хука.



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

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

Заключение


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

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

Комментарии (9)


  1. vtvz_ru
    05.11.2017 20:00

    Раньше тоже пользовался HipChat. Но столкнулся с проблемой, что на мобилках приложение не особо держится и вычищается. Из-за чего все уведомления пролетали мимо. Мы себе этого позволить не могли, так как в таких сообщениях могла содержаться очень важная на текущий момент информация. Что я только не делал (вроде там галочка была, чтобы он не терял соединение при сворачивании, но проблемы она не решала; это было давно, возможно, сейчас проблема исправлена). В итоге перешли в Slack. С тех пор таких проблем не наблюдалось.


    Говоря про Bitbucket веб-хуки. Это хорошо, чтобы только уведомить сервер о том, что произошел пуш. И не более. У меня тоже происходила сборка по хуку, но продолжительность сборки была дольше, чем Bitbucket позволял мне, поэтому происходил timeout и еще две попытки, что запукало 3 сборки почти параллельно. Сейчас сделано так: веб-хук только записывает в файл, что произошло изменение (в моем случае пишет еще и в какую ветку, чтобы можно было собрать dev и release отдельно; у меня информация хранится в JSON массиве, чтобы если я запушил сразу в две ветки, прошло две независимых сборки). А по крону запускается ежеминутно скрипт, который и запускает сборку.


    Сейчас в планах запуск тестов и сборку проекта перенести в Pipelines Это почти как CI, но попроще и интегрировано прямо в bitbucket. Там 50 минут в месяц бесплатно; для малых проектов хватит. Если хочется больше, то можно докупить 1000 минут за $10.


    1. Ticman Автор
      06.11.2017 11:27

      Да, битбакет выставляет таймаут запросу в 10 секунд. Когда порядка тысячи тестов в приложении, разумеется, тесты не успеют выполниться за такое время. Но на то у нас есть очереди, которые в laravel'е настриваются довольно легко. Забыл об этом упомянуть в статье, спасибо!
      Я думал и даже пробывал настроить пайплайны, но, как вы сами сказали, бесплатное время билда — 50 минут. А билдить проект при каждом коммите, по крайней мере, у меня, в итоге займет на порядок больше времени в конце месяца.


      1. vtvz_ru
        06.11.2017 13:29

        Там есть кастомные настройки, которые можно запустить вручную. Там в этом плане все достаточно гибко.
        Насколько я знаю, запускается он только на последний коммит и только так, как настроишь. У нас, например, ветка master для разработки, которая ни на что не влияет. Там особо не нужно ничего запускать. А вот ветка release уже предназначена для кода в продакшн. Вот там вот уже и запускать тесты, сборку и прочее.
        Я хочу сначала попробовать сделать так, чтобы все запускалось через pipeline, но работало как раньше, через веб-хуки. Если времени хватит, то перейдем в pipeline полностью. Или если начальство не по-жадничает $10 в месяц на дополнительные минуты)


        1. Ticman Автор
          06.11.2017 14:27

          Не знал про кастомные настройки пайплайнов, дякую.


  1. lxsmkv
    06.11.2017 02:50

    прочитав слово «самотестируемая» подумал про что-то из разряда model based testing. A на самом деле это просто DIY система CI. Тесты тут не генерируются сами. Но как инструкция для начинающих девопсов — полезно.


  1. antondukhanin
    06.11.2017 11:37

    Интересно, но есть подозрение, что большую часть логики (в частности сработка при пуше на определенную ветку) можно было перенести в piplines гораздо меньшей кровью (с меньшим количеством кода и контроллеров, буквально в 10 строк). В gitlab-ci это точно есть, стоит думать что и у bitbucket в его pipelines. Логика интеграции лежала бы отдельно от логики приложения.

    Что касается интеграции с чатом — думаю, тоже много готовых механизмов. А если нет — то можно было бы так же накрутить HipChatService в laravel в тестах.


    1. vtvz_ru
      06.11.2017 13:35

      Pipelines условно платная услуга. 50 минут в месяц может не хватить. Из этих соображений некоторые пилят свои велосипеды (как я и автор статьи).
      Гитлаб хоть и бесплатный, но для него лучше купить второй сервер, чтобы сильно основной не нагружать. А, откровенно говоря, не все могут позволить себе иметь два сервера (как я и, возможно, автор статьи)


      Но Вы абсолютно правы. Специализированные, изначально интегрированные и проверенные временем и тысячами разработчиками инструменты намного лучше и надежнее, чем подобные решения.


    1. Borro
      06.11.2017 14:47

      Я для хипчата использовал либу https://github.com/gorkalaucirica/HipchatAPIv2Client. Очень удобная.


  1. Borro
    06.11.2017 14:53

    Только заметил, что она уже UNMANTAINED