Описание
Сервер непрерывной интеграции (CI - Continuous Integration) предназначен для автоматической периодической сборки проектов, запуска модульных тестов, проверки кода на наличие распространенных ошибок и формирования ряда других отчетов. В качестве CI-сервера будет использоваться CruiseСontrol (CC) с дополнением phpUnderControl. CС запускается в качестве демона, периодически опрашивает SVN-репозиторий и, в случае наличия изменений, создает локальную копию, над которой в дальнейшем производится ряд определенных действий. Для выполнения этих действий в нужной последовательности используется утилита Ant (также поддерживается работа с его php-клоном phing), которая поставляется в комплекте с CC.
Дополнение phpUnderControl предоставляет консольную утилиту phpuc которая дополняет CC рядом шаблонов, а также позволяет быстро добавлять/удалять проекты в CC. В phpUnderControl встроена поддержка проверки синтаксиса php-кода, а также следующих утилит:
- phpunit
- автоматизированное тестирование приложения
- phpdoc
- PhpDocumentor - автоматическая генерация документации
- phpcb
- PHP_CodeBrowser - просмотр кода онлайн (включая подсветку и описание проблемных мест, таких как: нарушение стандартов кодирования, дублирование кода, высокая сложность кода)
- phpcs
- PHP_CodeSniffer - обнаружение нарушений стандартов кодирования
- ezcGraph
- простроение графиков изменения контролируемых параметров с течением времени
Кроме того есть возможность добавить поддержку других утилит, таких как:
- phpmd
- анализ сложности исходного кода
- phpcpd
- поиск дублирующегося кода
Скриншоты
Главная страница: список проектов, ручной запуск сборок:
Страница с результатами выполнения тестов phpunit:
Страница метрик, изменение контролируемых параметров от сборки к сборке:
Страница, отображающая покрытие кода тестами:
Онлайн-просмотр кода с подсветкой и описанием проблемных участков (оформление, сложность, дублирование и пр.):
Онлайн документация:
Страница отчета о нарушениях стандартов кодирования:
Страница отчета сложности кода, дублирования кода и других проблем:
Просмотр продублированного кода:
Установка CruiseСontrol
Для работы сервера CC нужна java-машина, устанавливаем, если еще не установлена:
sudo apt-get install sun-java6-bin
Затем следует скачать архив с последней версией CC с сайта http://cruisecontrol.sourceforge.net/download.html (на момент написания последней была версия 2.8.4) и распаковать в локальный каталог (в данном примере /opt):
sudo unzip cruisecontrol-bin-2.8.4.zip -d /opt
Для удобства обновлений программы можно создать символическую ссылку cruisecontrol, которая будет указывать на используемую версию данного ПО:
cd /opt
sudo ln -s cruisecontrol-bin-2.8.4 cruisecontrol
Теперь, когда CC-сервер установлен, можно настроить его автоматический запуск при старте системы. Для этого следует добавить нового пользователя, от имени которого будет запускаться процесс CC, разместить стартовый скрипт cruisecontrol (приложение А) в каталоге /etc/init.d/ (для debian-подобных систем), установить необходимые для его запуска права и, собственно, настроить автозапуск:
sudo adduser --system --group --home /opt/cruisecontrol cruisecontrol
sudo chown -R cruisecontrol:cruisecontrol /opt/cruisecontrol-bin-2.8.4 /opt/cruisecontrol
sudo cp cruisecontrol /etc/init.d/cruisecontrol
sudo chmod 755 /etc/init.d/cruisecontrol
sudo update-rc.d cruisecontrol defaults
Теперь вручную сервер CC можно запустить командой
sudo /etc/init.d/cruisecontrol start
Остановить:
sudo /etc/init.d/cruisecontrol stop
Для отладочных целей запуск сервера CC можно выполнить командой (из каталога /opt/cruisecontrol/):
sudo ./cruisecontrol.sh -configfile /opt/cruisecontrol/config.xml -webport 8080 -jmxport 8082 -rmiport
После успешного запуска CC-сервера должен быть доступен адрес http://localhost:8080.
Возможна проблема при запуске CC сервера, когда в файл /opt/cruisecontrol/cruisecontrol.sh из стартового скрипта не передается переменная окружения JAVA_HOME. Одним из вариантов решения данной проблемы может быть запись строки JAVA_HOME=/usr/lib/jvm/java-6-sun в общесистемный файл /etc/environment. Также, в качестве временного решения, можно указать значение этой переменной непосредственно в файле /opt/cruisecontrol/cruisecontrol.sh. Однако при использовании в стартовом скрипте команды su cruisecontrol -p -c ... этой проблемы возникать не должно.
Установка phpUnderControl и необходимых модулей
Прежде чем переходить к установке phpuc следует проверить наличие требуемых модулей, и установить/обновить недостающие (может потребоваться обновление xdebug и pear installer):
sudo apt-get install php5-xdebug
sudo pear install PhpDocumentor
sudo pear install PHP_CodeSniffer
sudo pecl install pecl/xdebug
pear channel-discover pear.phpunit.de
pear install phpunit/PHPUnit
sudo pear install phpunit/phpcpd
sudo pear upgrade pear
sudo pear install phpunit/PHP_CodeBrowser
Далее можно перейти к установке phpuc (на момент написания использовалась версия 0.6.1beta1):
sudo pear channel-discover components.ez.no
sudo pear channel-discover pear.phpundercontrol.org
sudo pear install --alldeps phpuc/phpUnderControl-beta
А также установить дополнительные модули, поддержка которых в phpUnderControl на данный момент реализована лишь частично (требуется ручная правка конфигов):
sudo pear channel-discover pear.phpmd.org
sudo pear channel-discover pear.pdepend.org
sudo pear install --alldeps phpmd/PHP_PMD
sudo pear install --alldeps phpunit/PHPCPD
phpuc представляет собой консольную утилиту для взаимодействия с установленной версией CC. Следующая команда скопирует набор дополнительных файлов и шаблонов для веб-интерфейса в каталоги с установленной копией CC:
sudo phpuc install /opt/cruisecontrol
Добавление проектов
Утилита phpuc также предоставляет команды для быстрого добавления проектов под управление ci-сервера (следующая команда - это одна строка, отформатированная для удобства чтения):
sudo phpuc project
--ant-script /opt/cruisecontrol/apache-ant-1.7.0/bin/ant
--version-control svn
--version-control-url file://localhost/home/user/svn/myProjectName/trunk
--coding-guideline Zend
--source-dir application
--test-dir tests
--project-name myProjectName
/opt/cruisecontrol
В результате работы данной команды
- в каталоге projects CC-сервера будут созданы подкаталоги для указанного проекта
- в конфигурационный файл СС будет добавлена новая секция <project/> (см. приложение Б). Стоит отметить что нода <shedule/> отвечает за выполнение запланированных для проекта действий (в данном примере каждые 300 сек. будет запускаться указанный ant-скрипт)
- в каталоге проекта будет создан файл build.xml (см. приложение В), содержащий ряд заданий для последующих регулярных сборок проекта
- выполнена команда svn checkout
Для удаления проекта следует использовать команду
sudo phpuc delete myProjectNameНастройка проектов
На данном этапе мы должны получить готовую автоматическую систему сборки и тестирования проектов, однако она может нуждаться в дополнительных доработках. В частности для цели lint (файл build.xml проекта) можно исключить из списка проверяемых путей некоторые каталоги (например каталог с дистрибутивом Zend) - это уменьшит время сборки:
<target name="lint">
<apply executable="php" dir="${basedir}/source" failonerror="on" logerror="on">
<arg line="-l"/>
<fileset dir="${basedir}/source">
<include name="**/*.php"/>
<exclude name="library/Zend/**/*.php"/>
</fileset>
</apply>
</target>
Для публикации отчетов о проблемах кода (phpmd) следует добавить новую цель:
...
<target name="phpmd">
<exec executable="phpmd" dir="${basedir}/source">
<arg line="application
xml
codesize,unusedcode,naming
--reportfile ${basedir}/build/logs/phpmd.xml"/>
</exec>
</target>
...
А также не забыть указать её в зависимостях цели сборки по умолчанию:
<target name="build" depends="checkout,lint,php-documentor,php-codesniffer,phpunit,phpcpd,phpmd,pdepend"/>
Для публикации отчетов о дублировании кода (phpcpd) нужно также добавить вручную новую цель в файл build.xml:
...
<target name="phpcpd" >
<exec executable="phpcpd" failonerror="false">
<arg line="--log-pmd ${basedir}/build/logs/pmd-cpd.xml
${basedir}/source/application" />
</exec>
</target>
...
А также не забыть указать её в зависимостях цели сборки по умолчанию:
<target name="build" depends="checkout,lint,php-documentor,php-codesniffer,phpunit,phpcpd,phpmd,pdepend"/>
Для построении графиков внутренних зависимостей кода (pdepend) нужно добавить следующую цель в build.xml:
...
<target name="pdepend" depends="lint">
<exec executable="pdepend" dir="${basedir}/source" logerror="on">
<arg line="--summary-xml=${basedir}/build/logs/pdepend.xml
--jdepend-chart=${basedir}/build/graph/jdepend.svg
--overview-pyramid=${basedir}/build/graph/overview-pyramid.svg
--coderank-mode=inheritance,property,method
application" />
</exec>
</target>
...
А также не забыть указать её в зависимостях цели сборки по умолчанию:
<target name="build" depends="checkout,lint,php-documentor,php-codesniffer,phpunit,phpcpd,phpmd,pdepend"/>
Кроме того, для публикации этих графиков на странице метрик, следует добавить в ноду <publishers/> соответствующего проекта в файле config.xml следующую строку:
<publishers>
...
<artifactspublisher subdirectory="graph" dest="artifacts/${project.name}" dir="projects/${project.name}/build/graph"/>
...
</publishers>
Можно добавить и другие цели, которые могут потребоваться для сборки проекта, например создание необходимых каталогов, установка прав на файлы и каталоги, подготовка БД, выполнение других скриптов, минимизация css, js и др.
Наконец, есть возможность рассылать уведомления об удачных и неудачных сборках на email, jabber и др. Ниже пример для рассылки e-mail уведомлений:
<publishers>
...
<email mailhost="localhost"
returnaddress="cruisecontrol@my.marketgid.net"
buildresultsurl="http://localhost:8080/cruisecontrol/buildresults/mgtest"
skipusers="true" spamwhilebroken="true">
<map alias="management" address="root@localhost" />
<map alias="qa" address="user@localhost" />
<map alias="developer" address="user@my.marketgid.net" />
<always address="management" />
<success address="qa" />
<failure address="developer" reportWhenFixed="true" />
</email>
...
</publishers>
В данном случае определены 3 группы получателей; первая группа management получает уведомления о всех сборках, вторая группа qa только об удачных сборках, и третья группа developer о неудачных.
Примеры окончательного варианта файлов настроек config.xml и build.xml в приложении Г и приложении Д.
Ссылки
- http://habrahabr.ru/blogs/php/68571/
- на русском, есть неточности
- http://nohn.net/blog/view/id/cruisecontrol_ant_and_phpunit
- настройка phpunit+ant, рассылка email-ов
- http://www.phpunit.de/manual/3.2/en/continuous-integration.html
- пример cli для phpuc (создание проекта в сс)
- http://recursive-design.com/blog/2011/05/13/continuous-integration-for-php-with-php-under-control/
- подробное руководство
- http://techportal.ibuildings.com/2009/03/03/getting-started-with-phpundercontrol/
- неполный мануал и пример выполнения sql-кода в ant
- http://topecoders.blogspot.com/2010/05/how-to-configure-phpundercontrol.html
- пример cli phpuc для zend, добавление нового таба
- http://criticallog.thornet.net/2010/03/02/integrate-php_depend-with-phpundercontrol/
- настройка pdepend (+см. комментарии)
- http://zendframework.ru/articles/continuous-integration-and-cruisecontrol
- CC с учетом специфики zf
Приложение А - скрипт для автоматического запуска CC при загрузке ОС
Скрипт отличается от источника установленными значениями переменных, а также опцией команды su -p, что решает проблему передачи переменной окружения JAVA_HOME серверу CC. Стоить отметить что инициализацию переменных можно вынести в отдельный файл /etc/default/cruisecontrol. Значения, указанные в этом файле, будут иметь приоритет (имеет смысл воспользоваться этой возможностью, если принято решение собрать deb-пакет для установки сервера CC).
#!/bin/sh
#content of /opt/cruisecontrol/init script
# chkconfig: 345 99 05
# description: CruiseControl build loop (see /home/tools)
# CruiseControl Unix Startup Script Version 2.1
#
# based on http://confluence.public.thoughtworks.org/display/CC/UnixStartupScriptVersion1.x
# adapted for multiple projects
# also based on the file attached to the above page created by Jerome Lacoste
#
# CruiseControl startup: Startup and kill script for Cruise Control
#
###################################################################################################
# USER CONFIGURATION
#
# Fill in these values for your Cruise Control setup
# What user will Cruise Control run as? The user will need permission to write and modify files
# in the next entries.
CC_USER=cruisecontrol
# Where is the CC startup script located?
CC_INSTALL_DIR=/opt/cruisecontrol
# In what directory is the config.xml file located for CC?
# default: CC_WORK_DIR=$CC_INSTALL_DIR
CC_WORK_DIR=$CC_INSTALL_DIR
# Where will the cruisecontrol.log file be located?
# default: CC_LOGFILE_DIR=$CC_INSTALL_DIR
CC_LOGFILE_DIR=$CC_INSTALL_DIR
#######################
# ENVIRONMENT ADDITIONS
# Add environement variables here that are needed by your build.
# example:
# export JAVA_HOME=/usr/local/java
#
# or like this for local variables ONLY used in this file:
# JAVA_HOME=/usr/local/java
JAVA_HOME=/usr/lib/jvm/java-6-sun
export JAVA_HOME
# Add path to additional executables needed for project build. See PATH entry below for base config.
# No additional action taken when blank.
PATH_ADDITIONS=
##############################
# CRUISE CONTROL PORT SETTINGS
# Port for Jetty reporting application. You can access it by going to http://localhost:8080
# default CC_WEBPORT=8080
CC_WEBPORT=8080
# JMX port for webapp and Java Management eXtensions (JMX). You can access it by going to http://localhost:8080
# Change only if this port is in use as the webapp will also need modification.
# default CC_JMXPORT=8082
CC_JMXPORT=8082
# RMI port for control via Java's Remote Management Interface (RMI)
# Leave blank to disable.
CC_RMIPORT=
###################################################################################################
# DO NOT MODIFY ENTRIES BELOW THIS LINE
NAME=cruisecontrol
DESC="CruiseControl - continuous integration build loop"
PATH=/sbin:/usr/sbin:/usr/bin:/bin
# add additions if variable has text defined
if [ -n "$PATH_ADDITIONS" ]; then
PATH=$PATH_ADDITIONS:$PATH
fi
export PATH
CC_DAEMON=$CC_INSTALL_DIR/cruisecontrol.sh
CC_CONFIG_FILE=$CC_WORK_DIR/config.xml
CC_LOG_FILE=$CC_LOGFILE_DIR/cruisecontrol.log
CC_COMMAND="$CC_DAEMON -configfile $CC_CONFIG_FILE -webport $CC_WEBPORT -jmxport $CC_JMXPORT -rmiport $CC_RMIPORT"
# overwrite settings from default file
if [ -f /etc/default/cruisecontrol ]; then
. /etc/default/cruisecontrol
fi
# does the executable exist?
test -f $CC_DAEMON || (echo "The executable $CC_DAEMON does not exist!" && exit 0)
if [ `id -u` -ne 0 ]; then
echo "Not starting/stopping $DESC, you are not root."
exit 4
fi
# Get the PID output from the startup script
if [ -f $CC_INSTALL_DIR/cc.pid ]; then
CC_PID=`cat $CC_INSTALL_DIR/cc.pid`
else
echo "No cc.pid file found. CC process may not be controllable from this script!"
fi
case "$1" in
'start')
cd $CC_INSTALL_DIR
# echo "CC environtment at startup" > cc.startup.env
# env >> cc.startup.env
su $CC_USER -p -c "/bin/sh -c \"$CC_COMMAND >> $CC_LOG_FILE 2>&1\"" & RETVAL=$?
echo "$NAME started with jmx on port ${CC_JMXPORT}"
;;
'stop')
if [ -n "$CC_PID" ] && ps -p ${CC_PID} > /dev/null ; then
kill -9 ${CC_PID}
$0 status
RETVAL=$?
else
echo "$NAME is not running"
RETVAL=1
fi
;;
'status')
if [ -n "$CC_PID" ] && ps -p ${CC_PID} > /dev/null ; then
echo $NAME \(pids $CC_PID\) is running
RETVAL=0
else
echo "$NAME is stopped"
RETVAL=1
fi
;;
'restart')
$0 stop && $0 start
RETVAL=$?
;;
*)
echo "Usage: $0 { start | stop | status | restart }"
exit 1
;;
esac
#echo ending $0 $$....
exit 0;
Приложение Б - project-секция config.xml для проекта myProjectName
<cruisecontrol>
...
<project name="myProjectName" buildafterfailed="false">
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
<modificationset>
<svn localWorkingCopy="/opt/cruisecontrol/projects/myProjectName/source"/>
</modificationset>
<bootstrappers>
<svnbootstrapper localWorkingCopy="/opt/cruisecontrol/projects/myProjectName/source"/>
</bootstrappers>
<schedule interval="300">
<ant buildfile="projects/${project.name}/build.xml" antscript="/opt/cruisecontrol/apache-ant-1.7.0/bin/ant"/>
</schedule>
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/build/logs/"/>
</log>
<publishers>
<artifactspublisher dir="projects/${project.name}/build/api" dest="artifacts/${project.name}" subdirectory="api"/>
<artifactspublisher dir="projects/${project.name}/build/coverage" dest="artifacts/${project.name}" subdirectory="coverage"/>
<execute command="phpcb --log projects/${project.name}/build/logs --source projects/${project.name}/source/application --output projects/${project.name}/build/php-code-browser"/>
<artifactspublisher dir="projects/${project.name}/build/php-code-browser" dest="artifacts/${project.name}" subdirectory="php-code-browser"/>
<execute command="/usr/bin/phpuc graph logs/${project.name} artifacts/${project.name}"/>
</publishers>
</project>
...
</cruisecontrol>
Приложение В - сгенерированный конфигурационный файл build.xml для проекта myProjectName
<?xml version="1.0" encoding="UTF-8"?>
<project name="myProjectName" default="build" basedir=".">
<target name="build" depends="checkout,lint,php-documentor,php-codesniffer,phpunit"/>
<target name="checkout">
<exec executable="svn" dir="${basedir}/source" failonerror="on">
<arg line="up"/>
</exec>
</target>
<target name="lint">
<apply executable="php" dir="${basedir}/source" failonerror="on" logerror="on">
<arg line="-l"/>
<fileset dir="${basedir}/source">
<include name="**/*.php"/>
</fileset>
</apply>
</target>
<target name="php-documentor" depends="lint">
<exec executable="phpdoc" dir="${basedir}/source" logerror="on">
<arg line="--title '${ant.project.name}' -ue on -t ${basedir}/build/api -d application -tb '/usr/share/php/data/phpUnderControl/data/phpdoc' -o HTML:Phpuc:phpuc"/>
</exec>
</target>
<target name="php-codesniffer" depends="lint">
<exec executable="phpcs" dir="${basedir}/source" output="${basedir}/build/logs/checkstyle.xml" error="/tmp/checkstyle.error.log">
<arg line="--report=checkstyle --standard=Zend application"/>
</exec>
</target>
<target name="phpunit" depends="lint">
<exec executable="phpunit" dir="${basedir}/source" failonerror="on">
<arg line=" --log-junit ${basedir}/build/logs/phpunit.xml --coverage-clover ${basedir}/build/logs/phpunit.coverage.xml --coverage-html ${basedir}/build/coverage tests"/>
</exec>
</target>
</project>
Приложение Г - пример окончательного варианта файла настройки проекта в config.xml
<?xml version="1.0"?>
<cruisecontrol>
...
<project name="myProjectName" buildafterfailed="false">
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
<modificationset>
<svn localWorkingCopy="/opt/cruisecontrol/projects/myProjectName/source"/>
</modificationset>
<bootstrappers>
<svnbootstrapper localWorkingCopy="/opt/cruisecontrol/projects/myProjectName/source"/>
</bootstrappers>
<schedule interval="300">
<ant buildfile="projects/${project.name}/build.xml" antscript="/opt/cruisecontrol/apache-ant-1.7.0/bin/ant"/>
</schedule>
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/build/logs/"/>
</log>
<publishers>
<artifactspublisher dir="projects/${project.name}/build/api" dest="artifacts/${project.name}" subdirectory="api"/>
<artifactspublisher dir="projects/${project.name}/build/coverage" dest="artifacts/${project.name}" subdirectory="coverage"/>
<execute command="phpcb --log projects/${project.name}/build/logs --source projects/${project.name}/source --output projects/${project.name}/build/php-code-browser"/>
<artifactspublisher dir="projects/${project.name}/build/php-code-browser" dest="artifacts/${project.name}" subdirectory="php-code-browser"/>
<execute command="/usr/bin/phpuc graph logs/${project.name} artifacts/${project.name}"/>
<artifactspublisher subdirectory="graph" dest="artifacts/${project.name}" dir="projects/${project.name}/build/graph"/>
<email mailhost="localhost"
returnaddress="cruisecontrol@my.marketgid.net"
buildresultsurl="http://localhost:8080/cruisecontrol/buildresults/myProjectName"
skipusers="true" spamwhilebroken="true">
<map alias="management" address="root@localhost" />
<map alias="qa" address="user@localhost" />
<map alias="developer" address="user@my.marketgid.net" />
<always address="management" />
<success address="qa" />
<failure address="developer" reportWhenFixed="true" />
</email>
</publishers>
</project>
...
</cruisecontrol>
Приложение Д - пример окончательного варианта файла build.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="mgtest" default="build" basedir=".">
<target name="build" depends="checkout,mkdirs,lint,php-documentor,php-codesniffer,phpunit,phpcpd,phpmd,pdepend"/>
<target name="checkout">
<exec executable="svn" dir="${basedir}/source" failonerror="on">
<arg line="up"/>
</exec>
</target>
<target name="lint">
<apply executable="php" dir="${basedir}/source" failonerror="on" logerror="on">
<arg line="-l"/>
<fileset dir="${basedir}/source">
<include name="**/*.php"/>
<exclude name="library/Zend/**/*.php"/>
</fileset>
</apply>
</target>
<target name="mkdirs">
<mkdir dir="${basedir}/source/cache/models"/>
</target>
<target name="php-documentor" depends="lint">
<exec executable="phpdoc" dir="${basedir}/source" logerror="on">
<arg line="--title '${ant.project.name}'
-ue on
-t ${basedir}/build/api
-d application
-tb '/usr/share/php/data/phpUnderControl/data/phpdoc'
-o HTML:Phpuc:phpuc"/>
</exec>
</target>
<target name="php-codesniffer" depends="lint">
<exec executable="phpcs" dir="${basedir}/source" output="${basedir}/build/logs/checkstyle.xml" error="/tmp/checkstyle.error.log">
<arg line="--report=checkstyle
--standard=Zend
application"/>
</exec>
</target>
<target name="phpmd">
<exec executable="phpmd" dir="${basedir}/source">
<arg line="application
xml
codesize,unusedcode,naming
--reportfile ${basedir}/build/logs/phpmd.xml"/>
</exec>
</target>
<target name="phpunit" depends="lint">
<exec executable="phpunit" dir="${basedir}/source/tests" failonerror="false">
<arg line="--bootstrap ${basedir}/source/tests/bootstrap.php
--log-junit ${basedir}/build/logs/phpunit.xml
--coverage-clover ${basedir}/build/logs/phpunit.coverage.xml
--coverage-html ${basedir}/build/coverage
."/>
</exec>
</target>
<target name="phpcpd" >
<exec executable="phpcpd" failonerror="false">
<arg line="--log-pmd ${basedir}/build/logs/pmd-cpd.xml
${basedir}/source/application" />
</exec>
</target>
<target name="pdepend" depends="lint">
<exec executable="pdepend" dir="${basedir}/source" logerror="on">
<arg line="--summary-xml=${basedir}/build/logs/pdepend.xml
--jdepend-chart=${basedir}/build/graph/jdepend.svg
--overview-pyramid=${basedir}/build/graph/overview-pyramid.svg
--coderank-mode=inheritance,property,method
application" />
</exec>
</target>
</project>