Jenkins: запуск PHPUnit из Codeception по Pull Reguest в Github и Allure-репорты

Автор: | 06/06/2019
 

Задача — запускать PHPUnit для тестов кода бекенда.

Сам PHPUnit будет запускаться из Codeception.

Задача в Jenkins должна триггериться из Github, при создании Pull Request — используем Github Pull-Request Builder плагин.

Для просмотра отчётов о тестах — используем Allure.

Jenkins запущен в Docker-контейнере, и все процессы будет запускать в контейнерах.

Проверка тестов

Что бы получить представление о том, как и что будет запускаться, и заодно проверить, что всё работает, как ожидается — сначала выполняем последовательный запуск на локальной машине.

PHP:

docker run -ti php:7.2 php --version
PHP 7.2.19 (cli) (built: Jun  1 2019 00:50:51) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

Выполняем init проекта:

docker run -ti -v $(pwd):/data/ php:7.2 /data/init --env=Development
Yii Application Initialization Tool v1.0
...
... initialization completed.

Теперь — Composer, что бы установить все зависимости:

docker run -ti -v $(pwd):/data/ --workdir /data composer install --optimize-autoloader --ignore-platform-reqs
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 118 installs, 0 updates, 0 removals
...
Generating optimized autoload files

И теперь сами юнит-тесты, для начала из каталога backend:

docker docker run -ti -v $(pwd):/data/ codeception/codeception run -c /data/backend/ unit
Codeception PHP Testing Framework v3.0.1
Powered by PHPUnit 7.5.9 by Sebastian Bergmann and contributors.
Running with seed:
Backend\tests.unit Tests (0) ----
Time: 99 ms, Memory: 16.00 MB
No tests executed!

Окей — тестов ещё нет, девелоперы допилят их потом, наша задача — make it works. Переходим к Jenkins.

Jenkins

Создаём Pipeline-джобу, настраиваем GitHub project:

Пишем первый, тестовый запуск:

node {
    
    stage('Clone repo') {
        git branch: "master", url: "git@github.com:orgname/reponame.git", credentialsId: "jenkins-github"
    }
    
    stage('Build app') {
        docker.image('php:7.2').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "php init --env=Development"
        }
    }
}

Запускаем:

ОК, работает — добавляем вызов Composer:

node {
    
    stage('Clone repo') {
        git branch: "master", url: "git@github.com:orgname/reponame.git", credentialsId: "jenkins-github"
    }
    
    stage('Build app') {
        docker.image('php:7.2').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "php init --env=Development"
        }
        docker.image('composer').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "composer install --optimize-autoloader --ignore-platform-reqs"
        }
    }
}

Запускаем, и:


— Installing orgname/itunes-receipt-validator (dev-master c8c11ba): Cloning c8c11ba7ba
Failed to download orgname/itunes-receipt-validator from source: Failed to execute git clone —no-checkout ‘git@github.com:orgname/itunes-receipt-validator.git’ ‘/var/lib/jenkins/workspace/BackendUnitTests/ProjectName/vendor/orgname/itunes-receipt-validator’ && cd ‘/var/lib/jenkins/workspace/BackendUnitTests/ProjectName/vendor/orgname/itunes-receipt-validator’ && git remote add composer ‘git@github.com:orgname/itunes-receipt-validator.git’ && git fetch composer

Now trying to download from dist
— Installing orgname/itunes-receipt-validator (dev-master c8c11ba): Downloading (connecting…)… Downloading (failed)

[Composer\Downloader\TransportException]
The «https://api.github.com/repos/orgname/itunes-receipt-validator/zipball/c8c11ba7ba1741c7dec248e6f9c0787cb146730a» file could not be downloaded (HTTP/1.1 404 Not Found)

Добавим Github Auth token:

...
        docker.image('composer').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "composer config -g github-oauth.github.com 64b***554"
            sh "composer install --optimize-autoloader --ignore-platform-reqs"
        }
...

Повторяем:

Работает.

Добавляем вызов самих тестов:

node {
    
    stage('Clone repo') {
        git branch: "master", url: "git@github.com:orgname/reponame.git", credentialsId: "jenkins-github"
    }
    
    stage('Build app') {
        docker.image('php:7.2').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "php init --env=Development"
        }
        docker.image('composer').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "composer config -g github-oauth.github.com 64b***554"
            sh "composer install --optimize-autoloader --ignore-platform-reqs"
        }
    }
    
    stage('Backend tests') {
        docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "codeception run -c backend/ unit"
        }        
    }
}

Jenkins Docker Plugin — The container started but didn’t run the expected command. Please double check your ENTRYPOINT.

Следующая ошибка — при запуске контейнера с codeception:


[Pipeline] withDockerContainer
Jenkins seems to be running inside container 81f6a8cbdb28a86b5e156a929ef06c2a68dd5c716910f0ab66073656c32c6472
$ docker run -t -d -u 0:0 -v /var/run/docker.sock:/var/run/docker.sock -w /var/lib/jenkins/workspace/BackendUnitTests/ProjectName —volumes-from 81f6a8cbdb28a86b5e156a929ef06c2a68dd5c716910f0ab66073656c32c6472 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** codeception/codeception cat
$ docker top 7e909bf25e232c951658478c1a82a93504061a4de4d90b2134680b3ca5f59f36 -eo pid,comm
ERROR: The container started but didn’t run the expected command. Please double check your ENTRYPOINT does execute the command passed as docker run argument, as required by official docker images (see https://github.com/docker-library/official-images#consistency for entrypoint consistency requirements).
Alternatively you can force image entrypoint to be disabled by adding option `—entrypoint=»`.

process apparently never started in /var/lib/jenkins/workspace/BackendUnitTests/ProjectName@tmp/durable-49ccb7e5

[Pipeline] End of Pipeline
ERROR: script returned exit code -2
Finished: FAILURE

Уже не раз встречался с такой проблемой при использовании некоторых Docker образов в Jenkins.

Окей.

Обычно просто собираю свой образ из Alpine или Debian/Ubuntu с нужным сервисом, но в этот раз было совсем лень, поэтому нашёл «решение».

В Jenkins issue, открытой, внезапно, ещё в 2016, и в которую до сих пор комментируют, предлагается задать --entrypoint="".

Но тогда не будет работать вызов sh "codeception run [...]".

Значит — надо найти исполняемый файл в Docker-образе Codeception. Можно было бы поискать Dockerfile образа и проверить его entrypoint, или запустить контейнер, и сделать docker inspect, но нашёл иначе, хотя и «костыльно».

Запускаем контейнер локально, переопределяем entrypoint в bash:

docker run -ti --entrypoint bash codeception/codeception
root@596960d14192:/project#

Попытался найти исполняемый файл codeception:

root@596960d14192:/project# find / -name codeception
/root/.composer/cache/files/codeception
/repo/vendor/codeception

Но это всё директории…

А как он вызывается?

Начал набирать code, кликнул TAB, и подставился сам исполняемый фал — им оказался codecept, а не codeception.

Обновляем вызов в пайплайне — добавляем --entrypoint="" и меняем вызов в sh:

...
    stage('Backend tests') {
        docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') {
            sh "/repo/codecept run -c backend/ unit"
        }        
    }
...

Запускаем:

Хорошо.

Пока дописал этот пост до этого места — девелоперы добавили «тестовый тест», запускаем с ним, плюс добавляем создание репортов в XML и HTML, см. документацию тут>>>.

...
    stage('Backend tests') {
        docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') {
            sh "/repo/codecept run -c backend/ unit --coverage-xml --coverage-html"
        }        
    }
...

Запускаем:

Allure reports

Далее — хочется добавить вывод репортов в Jenkins.

У нас уже есть Allure Reports Plugin для других билдов — попробуем прикрутить его к Codeception/PHPUnit.

Другой вариант — использовать Clover PHP Plugin.

Документация по Allure для Codeception — тут>>>.

Правим composer.json, добавляем установку Allure:

...
{
    "require": {
            ...
      "allure-framework/allure-codeception": ">=1.1.0"
    }
}
...

Обновляем конфиг backend/codeception.yml, добавляем extensions:

namespace: backend\tests
actor: Tester
paths:
    tests: tests
    log: tests/_output
    data: tests/_data
    helpers: tests/_support
settings:
    bootstrap: _bootstrap.php
    colors: true
    memory_limit: 1024M
modules:
    config:
        Yii2:
            configFile: 'config/test-local.php'
extensions: 
    enabled:
        - Yandex\Allure\Codeception\AllureCodeception
    config:
        Yandex\Allure\Codeception\AllureCodeception:
            deletePreviousResults: false
            outputDirectory: allure-results 
            ignoredAnnotations:
                - env        
                - dataprovider

В джобе добавляем обновление composer.lock (но лучше его перегенерить, и обновить в репозитории, что не выполнять update каждый раз во время запуска тестов, потом так и сделаю):

...
    stage('Build app') {
            ...
            sh "composer update --lock --ignore-platform-reqs"
            sh "composer install --optimize-autoloader --ignore-platform-reqs"
        }
    }
...

Добавляем сам Allure плагин и сбор репортов:

...
    stage('Backend tests') {
        docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') {
            sh "/repo/codecept run -c backend/ unit"
        }        
    }
    
    stage('Reports') {
        allure([
            includeProperties: false,
            jdk: '',
            properties: [],
            reportBuildPolicy: 'ALWAYS',
            results: [[path: 'backend/tests/_output/allure-results']]
        ])
    }
...

Запускаем билд:

И получаем репорт:

Github webhook

Последним шагом — надо настроить Github репозиторий, что бы он выполнял webhook при создании Pull Request к мастер-бранчу (в нашем случае develop-бранчу) и запускал юнит-тесты.

См. Jenkins: Github Pull-Request Builder плагин.

В настройки джобы добавляем GitHub project:

Создаём токен в Github, настраиваем плагин.

В Build Triggers включаем GitHub Pull Request Builder, отмечаем Use github hooks for build triggering.

В List of organizations. Their members will be whitelisted добавляем организацию Github (или отдельного юзера в Admin list, если нет организации в Github), и отмечаем Allow members of whitelisted organizations as admins:

Если вебхук не создался сам — добавляем вручную:

в Pyaload URL указываем https://<jenkins-URL>/ghprbhook/, указываем Let me select individual events, и выбираем события, при которых вебхук будет срабатывать — Issue comments и Pull requests:

Создаём PR:

Проверяем статус вебхука — должен был выполнить запрос к Jenkins-у:

Билд в Jenkins:

И PR прошёл:

В целом — на этом всё.

Далее — наводим марафет: выносим Jenkinsfile в репозиторий, всякие репозитории-бранчи в нём — в параметры Jenkins-джобы.

Финальный билд-скрипт выглядит так:

node {

    stage('Clone repo') {
        git branch: "${APPLICATION_BRANCH}", url: "${APPLICATION_URL}", credentialsId: "jenkins-github"
    }

    stage('Build app') {
        docker.image('php:7.2').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "php init --env=Development"
        }
        docker.image('composer').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "composer config -g github-oauth.github.com ${GITHUB_TOKEN}"
            sh "composer update --lock --ignore-platform-reqs"
            sh "composer install --optimize-autoloader --ignore-platform-reqs"
        }
    }

    try {
        stage('Backend tests') {
            docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') {
                sh "/repo/codecept run -c backend/ unit"
            }
        }

        stage('Frontend tests') {
            docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') {
                sh "/repo/codecept run -c frontend/ unit"
            }
        }
    } finally {
        stage('Reports') {
            allure([
                includeProperties: false,
                jdk: '',
                properties: [],
                reportBuildPolicy: 'ALWAYS',
                results: [[path: 'backend/tests/_output/allure-results'],[path: 'frontend/tests/_output/allure-results']]
            ])
        }
    }
}

Тут кроме бекенда добавлены тесты фронтенда, а вызов тестов завёрнут в try/catch, что бы всегда выполнять сбор репортов Allure (в finally), иначе, если тесты остановятся с FAILED — то до stage("Reports') выполнение скрипта не дойдёт, и сфейленные тесты не попадут в Allure.

Вызов скрипта в Jenkins:

И репорты — в backend один тест прошёл, во frontend один упал:

Готово.