25.10.2011

Подключаем PHP проект к серверу непрерывной интеграции

В прошлый раз мы обсуждали автоматизацию build проекта с использованием Phing. В этом посте тема будет продолжена: настало время подключить наш проект к серверу Continuous Integration.

Прежде чем начинать все устанавливать, настраивать и улучшать, скажем несколько слов о непрерывной интеграции в принципе. Постоянная интеграция — это практика, которая позволяет как можно быстрее выявить возможные интеграционные проблемы в проекте. Это достигается благодаря частым автоматизированным сборкам проекта. Как правило, участие человека здесь сводится к минимуму: на отдельном сервере по мере коммита нового кода в центральный репозиторий, запускаются различные тесты, которые могут сказать появились ли вместе с нововведениями какие-то проблемы.

Если это первый проект, который вы подключаете к серверу непрерывной интеграции, то скорее всего стоит использовать CruiseControl с установленным дополнением phpUnderControl. Для PHP-проектов такая конфигурация является (или покрайней мере являлась) своеобразном стандартом.

Переходим от слов к делу. Нам понадобится один сервер, свежие версии CruiseControl и phpUnderControl, Subversion (в нашем примере будет использована эта система контроля версий), пакеты sun-java6-jre, sun-java6-jdk. Кроме того далее будут упоминаться различные утилиты, которые также потребуется установить на сервере CI.

Начнем с установки CruiseControl:
unzip cruisecontrol-bin-2.8.4.zip -d /opt 
ln -s /opt/cruisecontrol-bin-2.8.4 /opt/cruisecontrol
Чтобы это приложение можно было легче запускать, останавливать и перезапускать, пригодится следующий скрипт, который изначально был опубликован в этой статье.
#!/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:
. /lib/lsb/init-functions
JAVA_HOME=/usr
NAME=cruisecontrol
DAEMON=/opt/cruisecontrol/cruisecontrol.sh
PIDFILE=/opt/cruisecontrol/cc.pid
 
test -x $DAEMON || exit 5
 
RUNASUSER=cruisecontrol
UGID=$(getent passwd $RUNASUSER | cut -f 3,4 -d:) || true
 
case $1 in
  start)
    log_daemon_msg "Starting Cruisecontrol server" "cc"
    if [ -z "$UGID" ]; then
        log_failure_msg "user \"$RUNASUSER\" does not exist"
        exit 1
    fi
    cd /opt/cruisecontrol/
    ./cruisecontrol.sh > /dev/null 2>&1
    log_end_msg $?
    ;;
  stop)
    log_daemon_msg "Stopping Cruisecontrol server" "cc"
    start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE
    log_end_msg $?
    rm -f $PIDFILE
    ;;
  restart|force-reload)
    $0 stop && sleep 2 && $0 start
    ;;
  status)
    pidofproc -p $PIDFILE $DAEMON >/dev/null
    status=$?
    if [ $status -eq 0 ]; then
        log_success_msg "Cruisecontrol server is running."
    else
        log_failure_msg "Cruisecontrol server is not running."
    fi
    exit $status
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|force-reload|status}"
    exit 2
    ;;
esac
Этот скрипт нужно поместить в /etc/init.d/cruisecontrol и установить права на исполнение. Нужно также обратить внимание на пользователя RUNASUSER=cruisecontrol.

Приступаем к установке phpUnderControl, и здесь все сводится к выполнению нескольких команд:
pear channel-discover components.ez.no
pear channel-discover pear.phpundercontrol.org
pear install --alldeps phpuc/phpUnderControl-beta
/usr/bin/phpuc install /opt/cruisecontrol
Проверим, что наш сервер постоянной интеграции запускается, запускаем его /etc/init.d/cruisecontrol start и смотрим в браузере http://localhost:8080/cruisecontrol/.

Теперь займемся конфигурацией самого проекта. У CruiseControl конфигурация всех проектов хранится в одном файле config.xml. Установим наш проект:
<cruisecontrol>
    <project name="Example" buildafterfailed="false">

        <plugin name="svnbootstrapper"
            classname="net.sourceforge.cruisecontrol.bootstrappers.SVNBootstrapper" />
        <plugin name="svn" classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN" />
        <plugin name="phing"
            classname="net.sourceforge.cruisecontrol.builders.PhingBuilder" />

        <listeners>
            <currentbuildstatuslistener file="logs/${project.name}/status.txt" />
        </listeners>

        <bootstrappers>
            <svnbootstrapper localWorkingCopy="projects/${project.name}/source/" />
        </bootstrappers>

        <modificationset quietperiod="0">
            <svn localWorkingCopy="projects/${project.name}/source/" />
        </modificationset>

        <schedule interval="120">
            <phing buildfile="projects/${project.name}/source/build-development.xml"
                uselogger="true" usedebug="false" />
        </schedule>

        <log dir="logs/${project.name}">
            <merge dir="projects/${project.name}/source/" />
        </log>

        <publishers>
            <artifactspublisher dir="projects/${project.name}/build/coverage"
                dest="logs/${project.name}" subdirectory="coverage" />
            <artifactspublisher dir="projects/${project.name}/public/apidocs"
                dest="logs/${project.name}" subdirectory="api" />

            <execute command="/usr/bin/phpuc graph logs/${project.name}" />
            <htmlemail mailhost="xx.xx.xx.xx" returnaddress="noreply@example.com"
                buildresultsurl="http://localhost:8080/buildresults/${project.name}"
                returnname="phpUnderControl server" logdir="logs/${project.name}">

                <failure address="fail@example.com" reportWhenFixed="true" />
            </htmlemail>
        </publishers>

    </project>
</cruisecontrol>
Обращу внимание на самое главное, исходных код будет располагаться в projects/example/source. Здесь же мы должны самостоятельно выполнить checkout проекта. Директивами svn, svnbootstrapper мы указываем, чтобы CruiseControl выполнял в каталоге с исходным кодом svn up. По мере появления новых изменений (в данном случае из центрального svn-репозитория) будет запущен процесс сборки проекта. Для сборки мы используем Phing и скрипт похожий на тот, который обсуждали в прошлом посте. Секция publishers помимо прочего содержит настройки для отправки уведомлений в случае неудачного билда.

Теперь стоит взглянуть на сам build-скрипт. Мы немного его дополним: добавим задачу по запуску проверки кода на «дурные запахи», поиск дублирования кода, и т.д.
<?xml version="1.0" encoding="UTF-8"?>
<project name="Example" default="default">
    <property name="build.dir" value="." />

    <!-- ============================================ -->
    <!-- (DEFAULT) Target: default                    -->
    <!-- ============================================ -->
    <target name="default"
        depends="run.tests, run.mess.detector, run.copypast.detector, run.phpcodesniffer, api.docs">
        <echo msg="All done." />
    </target>

    <!-- ============================================ -->
    <!-- Target: api.docs                             -->
    <!-- ============================================ -->
    <target name="api.docs">
        <phpdoc title="API Documentation" destdir="${build.dir}/public/apidocs"
            sourcecode="false" templatebase="/usr/share/php/data/phpUnderControl/data/phpdoc"
            output="HTML:Phpuc:phpuc">
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
        </phpdoc>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.tests                            -->
    <!-- ============================================ -->
    <target name="run.tests">
        <phpunit haltonfailure="true" haltonerror="true">
            <formatter type="xml" usefile="true"
                todir="${build.dir}/build/phpunit" />
            <formatter type="plain" usefile="false" />
            <batchtest>
                <fileset dir="${build.dir}/tests">
                    <include name="*Test.php" />
                </fileset>
            </batchtest>
        </phpunit>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.mess.detector                    -->
    <!-- ============================================ -->
    <target name="run.mess.detector">
        <phpmd>
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
            <formatter type="xml"
                outfile="${build.dir}/build/phpmd/phpmd.xml" />
            <formatter usefile="false" type="text" />
        </phpmd>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.copypast.detector                -->
    <!-- ============================================ -->
    <target name="run.copypast.detector">
        <phpcpd>
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
            <formatter type="pmd"
                outfile="${build.dir}/build/phpcpd/phpcpd.xml" />
            <formatter usefile="false" type="default" />
        </phpcpd>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.phpcodesniffer                   -->
    <!-- ============================================ -->
    <target name="run.phpcodesniffer">
        <phpcodesniffer standard="Zend" showSniffs="true"
            showWarnings="true">
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
            <formatter type="checkstyle"
                outfile="${build.dir}/build/phpcodesniffer/phpcodesniffer.xml" />
            <formatter type="summary" usefile="false" />
        </phpcodesniffer>
    </target>
</project>
Этот скрипт мы можем проверить, запустив самостоятельно phing -f build-development.xml. Конечно, чтобы все это работало, нужно установить phpdoc, phpcs, phpunit и xdebug.

Теперь можно выполнить несколько тестовых коммитов и понаблюдать за статистикой новых build'ов. Можно даже что-то специально сломать, чтобы проверить, как сервер непрерывной интеграции сообщит о проблеме.