O zaletach stosowania i czym w ogóle jest Continuous integration(CI) nie będę się w tym wpisie rozwodził. Wydaje się, że tyle już zostało powiedziane w tym temacie, że to wszystko staje się aż do bólu oczywiste w samej teorii. Dlatego dzisiejszy wpis chce by miał bardziej praktyczne podejście do tego. Od dłuższego czasu szukam jakiejś w miarę sensownej konfiguracji Jenkinsa pod projekty pisane w PHP. Niestety od momentu wprowadzenia Pipeline, wiele rozszerzeń dedykowanych pod PHP po prostu z tym nie współgra. Poniżej prezentuje co udało mi się ustalić i jak to wygląda w moim projekcie gdzie na pokładzie jest Symfony4 z testami pisanym w PHPUnit. Jednakże dla osób niezaznajomionych lub wciąż głodnych dodatkowych informacji, z całego serca mogę polecić książkę Continuous Integration: Improving Software Quality and Reducing Risk.
Na świeżej instalacji Jenkinsa 2.176.1 musiałem dodatkowo zainstalować następujące pluginy:
- PHP Built-in Web Server
- HTML Publisher - umożliwia publikowanie raportu z pokrycia kodu testami
- Clover - ustawia zachowania build'a na podstawie wyników z pokrycia kodu
- Clover PHP - PHP'owy wrapper na Clover
- Javadoc - prezentuje dokumentacje z phpdoc
- Static Analysis Utilities
- Checkstyle - przechwytuje raport z phpcs
- PMD - przechwytuje raport z phpmd
- DRY - przechwytuje raport z phpcpd
- Bitbucket Plugin - umożliwia przechwycenie webhook'a z Bitbucket (więcej o tym w dalszej części wpisu)
Poniżej Jenkinsfile
#!/usr/bin/env groovy
node {
stage('Get code from SCM') {
checkout(
[$class: 'GitSCM', branches: [[name: 'master']],
doGenerateSubmoduleConfigurations: false,
extensions: [],
submoduleCfg: [],
userRemoteConfigs: [[url: '[email protected]', credentialsId: 'user']]]
)
}
stage('Prepare') {
sh 'composer install'
sh 'bin/console assets:install'
sh 'bin/console cache:clear'
sh 'bin/console doctrine:database:create'
sh 'bin/console doctrine:migrations:migrate --no-interaction'
}
stage('PHP Syntax check') {
sh 'vendor/bin/parallel-lint --exclude vendor/ --exclude ./bin .'
}
stage('Symfony Lint') {
sh 'bin/console lint:yaml src'
sh 'bin/console lint:yaml tests'
sh 'bin/console lint:twig src'
sh 'bin/console lint:twig tests'
}
stage("PHPUnit") {
sh 'bin/phpunit --coverage-html build/coverage --coverage-clover build/coverage/index.xml'
}
stage("Publish Coverage") {
publishHTML (target: [
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: 'build/coverage',
reportFiles: 'index.html',
reportName: "Coverage Report"
])
}
stage("Publish Clover") {
step([
$class: 'CloverPublisher',
cloverReportDir: 'build/coverage',
cloverReportFileName: 'index.xml',
healthyTarget: [methodCoverage: 70, conditionalCoverage: 80, statementCoverage: 80], // optional, default is: method=70, conditional=80, statement=80
unhealthyTarget: [methodCoverage: 50, conditionalCoverage: 50, statementCoverage: 50], // optional, default is none
failingTarget: [methodCoverage: 0, conditionalCoverage: 0, statementCoverage: 0] // optional, default is none
])
}
stage('Checkstyle Report') {
sh 'vendor/bin/phpcs --report=checkstyle --report-file=build/logs/checkstyle.xml --standard=phpcs.xml --extensions=php,inc -wp || exit 0'
checkstyle pattern: 'build/logs/checkstyle.xml'
}
stage('Mess Detection Report') {
sh 'vendor/bin/phpmd . xml phpmd.xml --reportfile build/logs/pmd.xml || exit 0'
pmd canRunOnFailed: true, pattern: 'build/logs/pmd.xml'
}
stage('CPD Report') {
sh 'vendor/bin/phpcpd --log-pmd build/logs/pmd-cpd.xml --exclude bin --exclude vendor --exclude src/Migrations --exclude var . --progress || exit 0'
dry canRunOnFailed: true, pattern: 'build/logs/pmd-cpd.xml'
}
stage('Lines of Code') {
sh ' vendor/bin/phploc --count-tests --log-csv build/logs/phploc.csv --log-xml build/logs/phploc.xml . --exclude vendor --exclude src/Migrations --exclude var .'
}
stage('Software metrics') {
sh 'vendor/bin/pdepend --jdepend-xml=build/logs/jdepend.xml --jdepend-chart=build/dependencies.svg --overview-pyramid=build/overview-pyramid.svg --ignore=vendor,var,bin,build .'
}
stage('Generate documentation') {
sh 'vendor/bin/phpdox -f phpdox.xml'
}
stage('Publish Documentation') {
publishHTML (target: [
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: 'docs/html',
reportFiles: 'index.xhtml',
reportName: "PHPDox Documentation"
])
}
}
Wycinek z composer.json
"require-dev": {
"jakub-onderka/php-parallel-lint": "^1.0",
"pdepend/pdepend": "@stable",
"phploc/phploc": "^5.0",
"phpmd/phpmd": "@stable",
"sebastian/phpcpd": "^4.1",
"squizlabs/php_codesniffer": "*",
"symfony/debug-pack": "*",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^5.0",
"symfony/profiler-pack": "*",
"symfony/test-pack": "*",
"symfony/web-server-bundle": "4.3.*",
"theseer/phpdox": "^0.12.0"
},
A tak wygląda mój dashboard po tych buildach

W trakcie implementacji natknąłem się na dwa problemy. Pierwszy dotyczył ustawienia w Jenkinsie Credentials dla wygenerowanego klucza z dostępem do repozytorium Git. Pamiętajmy, że trzeba taki klucz publiczny utworzyć i zapisać w ustawieniach Jenkinsa - Credentials. Następnie w pliku Jenkinsfile w parametrze userRemoteConfigs należy ustawić credentialsId z identyfikatorem podanym w zakładce Credentials. Ciekawą opcją w przypadku używania akurat Bitbucket jest możliwość wygenerowania klucza z dostępem tylko do odczytu repozytorium. Zdecydowanie polecam tą opcję dla przyznania dostępu dla Jenkinsa. Inną problematyczną kwestią z jaką się spotkałem było przechwycenie webhooka z Bitbucket. Albo występował problem z uprawnieniami, albo serwer Jenkinsa klasyfikował żądanie jako nieprawidłowe. Gdy się z tym uporałem okazało się, że build dla gałęzi master idzie także wtedy gdy zostanie zrobiony push do innego brancha. Koszmar! Rozwiązanie, użyć pluginu bitbucket w Jenkinsie.
A Wy jaką macie konfiguracje ? ;-)