「CakePHP2+Jenkinsで継続的インテグレーション」について話してきました

週末に行われた「(CakePHPとか)PHPのテストについての勉強会」@Co-Edoで、「CakePHP2+Jenkinsで継続的インテグレーション」について話してきました。

今回は、CIの概念とかメリットに関してはこことか、ここなど先人のすばらしい資料に丸投げし(^^;、実際にCakePHP2でCIする際の流れやビルドの設定方法などを中心に話しました。割と突発的に行われた勉強会だった印象なのに非常に中身の濃い勉強会で僕自身もとても勉強になりました!

僕の発表は資料だけみてもちょっと分かりにくそうなので以下補足です。

Jenkinsの設定

実際の設定は以下を見て、このページに紹介されている「Job Template」をコピーする方法を使えば、あとで紹介するbuildファイル用の設定が一通り完了しているJobを簡単に作れるかと思います。

CakePHP2用のBuildファイル サンプル(Ant)

以下が、Template for Jenkins Jobs for PHP Projectsで紹介されているBuildファイルをCakePHP2用に修正したものです。

Jenkinsで実行する際は、tools-parallel というターゲットを実行することになります。

<?xml version="1.0" encoding="UTF-8"?>

<project name="name-of-project" default="build">
 <target name="build" depends="prepare,lint,phploc,pdepend,phpcb,phpmd-ci,phpcs-ci,phpcpd,phpunit"/>
 <target name="build-parallel" depends="prepare,lint,tools-parallel,phpunit"/>

 <target name="tools-parallel" description="Run tools in parallel">
  <parallel threadCount="2">
   <sequential>
    <antcall target="pdepend"/>
    <antcall target="phpmd-ci"/>
   </sequential>
   <antcall target="phpcpd"/>
   <antcall target="phpcs-ci"/>
   <antcall target="phploc"/>
   <antcall target="phpcb"/>
  </parallel>
 </target>

 <target name="clean" description="Cleanup build artifacts">
  <delete dir="${basedir}/build/api"/>
  <delete dir="${basedir}/build/code-browser"/>
  <delete dir="${basedir}/build/coverage"/>
  <delete dir="${basedir}/build/logs"/>
  <delete dir="${basedir}/build/pdepend"/>
 </target>

 <target name="prepare" depends="clean" description="Prepare for build">
  <mkdir dir="${basedir}/build/api"/>
  <mkdir dir="${basedir}/build/code-browser"/>
  <mkdir dir="${basedir}/build/coverage"/>
  <mkdir dir="${basedir}/build/logs"/>
  <mkdir dir="${basedir}/build/pdepend"/>
  <mkdir dir="${basedir}/build/phpdox"/>
 </target>

 <target name="lint" description="Perform syntax check of sourcecode files">
  <apply executable="php" failonerror="true">
   <arg value="-l" />

   <fileset dir="${basedir}/app">
    <include name="**/*.php" />
    <include name="**/*.ctp" />
    <modified />
   </fileset>
  </apply>
 </target>

 <target name="phploc" description="Measure project size using PHPLOC">
  <exec executable="phploc">
   <arg value="--log-csv" />
   <arg value="${basedir}/build/logs/phploc.csv" />
   <arg value="--exclude"/>
   <arg value="Test" />
   <arg value="--exclude"/>
   <arg value="Config/Migration" />
   <arg value="--exclude"/>
   <arg value="Plugin" />
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="pdepend" description="Calculate software metrics using PHP_Depend">
  <exec executable="pdepend">
   <arg value="--jdepend-xml=${basedir}/build/logs/jdepend.xml" />
   <arg value="--jdepend-chart=${basedir}/build/pdepend/dependencies.svg" />
   <arg value="--overview-pyramid=${basedir}/build/pdepend/overview-pyramid.svg" />
   <arg value="--exclude=${basedir}/app/Vendor,${basedir}/app/webroot" />
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="phpmd"
         description="Perform project mess detection using PHPMD and print human readable output. Intended for usage on the command line before committing.">
  <exec executable="phpmd">
   <arg path="${basedir}/app" />
   <arg value="text" />
   <arg value="${basedir}/build/phpmd.xml" />
  </exec>
 </target>

 <target name="phpmd-ci" description="Perform project mess detection using PHPMD creating a log file for the continuous integration server">
  <exec executable="phpmd">
   <arg path="${basedir}/app" />
   <arg value="xml" />
   <arg value="codesize,unusedcode,design,${basedir}/build/phpmd-naming.xml" />
   <arg value="--reportfile" />
   <arg value="${basedir}/build/logs/pmd.xml" />
   <arg value="--exclude" />
   <arg value="Test,Config,Vendor,Plugin" />
  </exec>
 </target>

 <target name="phpcs"
         description="Find coding standard violations using PHP_CodeSniffer and print human readable output. Intended for usage on the command line before committing.">
  <exec executable="phpcs">
   <arg value="--standard=${basedir}/build/phpcs.xml" />
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="phpcs-ci" description="Find coding standard violations using PHP_CodeSniffer creating a log file for the continuous integration server">
  <exec executable="phpcs">
   <arg value="--report=checkstyle" />
   <arg value="--report-file=${basedir}/build/logs/checkstyle.xml" />
   <arg value="--standard=${basedir}/build/phpcs.xml" />
   <arg value="--extensions=php"/>
   <arg value="-p"/>
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="phpcpd" description="Find duplicate code using PHPCPD">
  <exec executable="phpcpd">
   <arg value="--log-pmd" />
   <arg value="${basedir}/build/logs/pmd-cpd.xml" />
   <arg value="--exclude"/>
   <arg value="Test" />
   <arg value="--exclude"/>
   <arg value="Config/Migration" />
   <arg value="--exclude"/>
   <arg value="Plugin/" />
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="phpdox" description="Generate API documentation using phpDox">
  <exec executable="phpdox"/>
 </target>

 <target name="make-tmp-folders">
  <mkdir dir="${basedir}/app/tmp/"/>
  <mkdir dir="${basedir}/app/tmp/cache"/>
  <mkdir dir="${basedir}/app/tmp/cache/persistent"/>
  <mkdir dir="${basedir}/app/tmp/cache/models"/>
  <mkdir dir="${basedir}/app/tmp/cache/views"/>
  <mkdir dir="${basedir}/app/tmp/logs"/>
  <mkdir dir="${basedir}/app/tmp/tests"/>
  <mkdir dir="${basedir}/app/tmp/sessions"/>
 </target>

 <target name="migration">
  <exec executable="bash" failonerror="true" dir="${basedir}/app">
   <arg value="Console/cake"/>
   <arg value="Migrations.migration"/>
   <arg value="run"/>
   <arg value="all"/>
  </exec>
 </target>

 <target name="phpunit" depends="make-tmp-folders,migration" description="Run unit tests with PHPUnit">
  <exec executable="bash" failonerror="true" dir="${basedir}/app">
   <arg value="Console/cake"/>
   <arg value="test"/>
   <arg value="app"/>
   <arg value="AllTests"/>
   <arg value="--configuration"/>
   <arg path="${basedir}/build/phpunit.xml"/>
  </exec>
 </target>

 <target name="phpcb" description="Aggregate tool output with PHP_CodeBrowser">
  <exec executable="phpcb">
   <arg value="--log" />
   <arg path="${basedir}/build/logs" />
   <arg value="--source" />
   <arg path="${basedir}/app" />
   <arg value="--output" />
   <arg path="${basedir}/build/code-browser" />
   <arg value="--exclude=*/Config/*,*/Test/*,*/Vendor/*,*/View/*" />
  </exec>
 </target>
</project>

この設定を使用すると以下の処理が実行されます。

  1. lint - 文法チェック
  2. phploc(*) - プロジェクトの規模を測定
  3. pdpend (*) - パッケージ単位のメトリクスの測定(依存度とか)
  4. phpcb (*) - ソースコード毎の状態を解析
  5. phpmd - 静的解析(実装上の問題を検出)
  6. phpcs - コーディング規約チェック
  7. phpcpd - コピペコードの検出
  8. migration - DBのマイグレーション(cakedc/Migrations plugin)
  9. UnitTest - 単体テスト(CakePHP TestCase)

ツール毎に処理対象を除外する記述が異なってて以外と面倒なので、参考になれば嬉しいです。

おまけ

今進行中ののプロジェクトでは、以下の様なターゲットを作って、BDD Pluginでのストーリテストの実行やCapistranoでの自動デプロイもしてます。

<?xml version="1.0" encoding="UTF-8"?>

 <target name="migration-bdd">
  <exec executable="bash" failonerror="true" dir="${basedir}/app">
   <env key="APP_ENV" value="bdd" />
   <arg value="Console/cake"/>
   <arg value="Migrations.migration"/>
   <arg value="run"/>
   <arg value="all"/>
  </exec>
 </target>

 <target name="bdd-story" depends="make-tmp-folders,migration-bdd" description="Run Bdd Story tests">
  <exec executable="bash" failonerror="true" dir="${basedir}/app">
   <env key="APP_ENV" value="bdd" />
   <arg value="Console/cake"/>
   <arg value="Bdd.story"/>
   <arg value="--format=junit"/>
   <arg value="--out"/>
   <arg path="${basedir}/build/behat/"/>
  </exec>
 </target>

 <target name="deploy-staging" description="Deploy Appliction to Staging">
  <exec executable="cap" failonerror="true" dir="${basedir}">
   <arg value="staging"/>
   <arg value="deploy"/>
  </exec>
 </target>