[Из песочницы] Самотестируемая система с оповещениями на Laravel + Bitbucket + HipChat

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

hgub_vuoohsu7fhxidc6hrettl0.png

Перейдем во 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 комната создана, пишем логику


Создаем роут:

 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'
                    ]);
                });
            }
        );
    }
);



Контроллер:

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' => "{$author} только что запушил в репозиторий 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:

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 (документация). Важный момент, от имени какого пользователя будут запускаться команды, учтите это!

Далее код с комментариями наших сервисов.

Стягиваем изменения с битбакета:

run();

        // executes after the command finishes
        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        }

        return $process->getOutput();
    }
}


Запускаем тесты:

run();

        // executes after the command finishes
        if (!$process->isSuccessful()) {
            return $process->getIncrementalOutput();
        }

        return true;
    }
}


Отправка нотификаций сделана обычным curl запросом, к массиву данных применяем json_encode ()

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;
    }
}


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

5teuol5v5ehukw766c-qcdnwhmc.png

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

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

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

© Habrahabr.ru