about 2 years ago

在這邊有一個前提就是我們必須要有兩台Linux Server

  1. Production/Staging Server
  2. CI Server
  • Production/Staging Server使用CentOS6.4
  • CI Server使用Ubuntu14.04
  • 兩台Server已經安裝好Nginx/Apache, PHP, Mysql
  • (裝機SOP請看)
  • CI Server 使用Jenkins來執行自動化測試跟佈署的事項

廢話不多說,我們直接來開始


Step.1 先安裝CI Server需要的東西

$ sudo locale-gen zh_TW.UTF-8
$ sudo apt-get -y update
$ sudo apt-get -y install mysql-server php5-mysql php5-sqlite php5-mcrypt php5-intl php5-cli php5-fpm php5-curl nginx git unzip curl libcurl3-dev

Step.2 再安裝Jenkins需要的東西

sudo apt-get -y install openjdk-7-jre openjdk-7-jdk ant

openjdk-7-jreopenjdk-7-jdk是Java的開源版本,Jenkins是用Java寫的,所以一定要安裝

ant,就是Apapche Ant,Jenkins在執行工作排程的時候,可以透過Ant來執行一連串自動化的事情,使用Apapche Ant所提供的xml(預設叫做build.xml),來告訴Ant該做什麼事,而Jenkins只是呼叫它而已。


Step.3 去Jenkins官方的SOP來安裝看看

$ wget -q -O - https://jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add -
$ sudo sh -c 'echo deb http://pkg.jenkins-ci.org/debian binary/ > /etc/apt/sources.list.d/jenkins.list'
$ sudo apt-get update
$ sudo apt-get install jenkins

安裝完之後,就會自動啟動了。

直接連上http://yourhostname:8080,記得要對外開放8080port

安裝完,有幾點必須注意到的 :

Jenkins的家目錄(JENKINS_HOME)、設定檔都放在 /var/lib/jenkins

啟動、重啟、關閉 Jenkins

$ sudo /etc/init.d/jenkins restart|start|stop

Step.4 設定Jenkins所需要的權限

在sudoers加入Jenkins

$ sudo vim /etc/sudoers
/etc/sudoers
jenkins ALL=(ALL) NOPASSWD: ALL

Step.5 建構PHP專用的Jenkins環境

Jenkins能動之後,第一件事情就是來安裝Jenkins的套件
可以參照jenkins-php.org的Document來建立For PHP的Jenkins

這邊有幾項的建議安裝的套件给PHP專案使用
Ant Plugin
(通常已預設,用它所提供的xml格式告訴Jenkins該做什麼事,達到CI Server自動化)

Git plugin/Subversion Plug-in
(可依照專案來選擇版本控制工具)

xUnit plugin
(使用JUnit來輸出phpunit的log檔)

Clover PHP plugin
(能夠分析phpunit程式碼的測試覆蓋率)

HTML Publisher plugin
(用來發佈phpunit程式碼測試覆蓋率的報告)

Checkstyle Plug-in
(能夠分析你的PHP程式碼,是否有違反Coding Standard,可指定psr-*)

DRY Plug-in
(能夠分析你的程式碼是否重複)

JDepend Plugin
(能分析專案程式碼的程式碼規模跟複雜度)

PMD Plug-in
(分析專案的程式碼是否含有隱藏性風險,並提出警告)

Violations plugin
(檢查程式碼是否有嚴重缺陷)

Plot plugin
(可以藉由這個套件來產生圖表)

Publish Over SSH
(作為專案遠端遙控佈署之用)

這麼多個項目,那就通通把它安裝起來放著吧!!
另外不建議一次安裝太多套件,不然有時候機器等級小會反應不過來





接著來下載Jenkins的PHP樣板設定檔
下載PHP樣板設定檔的做法一樣可以參考jenkins-php.org

# $JENKINS_HOME = /var/lib/jenkins

$ cd $JENKINS_HOME/jobs
$ sudo mkdir php-template
$ cd php-template
$ sudo wget https://raw.github.com/sebastianbergmann/php-jenkins-template/master/config.xml
$ cd ..
$ sudo chown -R jenkins:jenkins php-template
$ sudo /etc/init.d/jenkins restart


Step.6 來測試Jenkins

先來建立新作業

因為我們一開始都不會設定,所以我們就去選擇剛剛下載的PHP樣板來嘗試設定看看

在設定檔裡面,只要把git設定https://github.com/sebastianbergmann/money
money這個github項目裡面包含了非常完整的apache ant的設定檔(build.xml),待會會解釋到build.xml怎麼設定

還有一個設定要注意,有時候產出報表,因為設定不正確的關係,導致Jenkins建置工作失敗,所以這邊也先勾選一下

接著儲存完之後,先按下啟用,開始讓Jenkins跑流程看看


接著我們用console來看看Jenkins執行了哪些事



結果建置失敗了...
原因是money這個github穩定的版本是1.5,我們回頭設定組態,設定完存檔

再讓Jenkins跑一次流程看看,ok!成功!


我們可以去看各式各樣的報表,這些就留給各位慢慢研究了



ok,Jenkins基本上能跑了,但這樣還不夠...
還有selenum server...
還有自動部屬...


Step.7 安裝Selenium Server環境

讓Selenium Server能夠動,首先要有三個東西
1. browser driver(firefox, chrome, ie ...etc)
2. selenium server
3. x-window

browser driver以chrome為例,可去google下載,也順便安裝一下chrome需要的套件

$ sudo apt-get -y install libxpm4 libxrender1 libgtk2.0-0 libnss3 libgconf-2-4
$ cd ~/
$ mkdir selenium-server
$ cd selenium-server

$ wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
$ sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
$ sudo apt-get update
$ sudo apt-get -y install google-chrome-stable
$ wget http://chromedriver.storage.googleapis.com/2.21/chromedriver_linux64.zip
$ unzip chromedriver_linux64.zip
$ sudo mv chromedriver /usr/local/bin/chromedriver
$ sudo chmod ugo+rx /usr/local/bin/chromedriver

selenium server可去selenium官方下載

$ cd ~/
$ mkdir selenium-server
$ cd selenium-server
$ wget http://selenium-release.storage.googleapis.com/2.52/selenium-server-standalone-2.52.0.jar

x-window,簡單來說是一套軟體,可以讓你的作業系統以圖形介面來顯示,Linux Server基本上是沒有x-window,沒有x-window,其實selenim server是沒辦法使用的。

先來安裝xvfb(virtual framebuffer X server for X Version 11)及相關套件,Xvfb可以直接處理程式的圖形化功能,並不會把圖像輸出到螢幕上,也就是說,就算你的電腦沒有x-window , 一樣可以執行任何圖形程式。

$ sudo apt-get -y install xvfb gtk2-engines-pixbuf xserver-xorg-core
$ sudo apt-get -y install xfonts-cyrillic xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable
$ sudo apt-get -y install imagemagick x11-apps

接著執行xvfb,這幾行的意思是,我把輸出結果丟到99號螢幕上,然後我們在定義一個99號螢幕去接

$ Xvfb :99 -screen 0 1024x768x24 -ac 2>&1 >/dev/null &
$ export DISPLAY=:99

再接著執行selenium server

$ java -Dwebdriver.chrome.driver=/usr/local/bin/chromedriver -jar selenium-server-standalone-2.52.0.jar -maxSession 10

直接連上http://yourhostname:4444/wd/hub,要看得話,記得要對外開放4444port
接著我們也可以直接用本機來測測看,連一下遠端的selenium server

$ cd phptheday23
$ php build/tools/phpunit_facebook_webdriver.phar --configuration build/phpunit.xml


遠端使用selenium server也OK!


Step.8 來實際把自己的project來跑一次測試+自動佈署吧

首先我們來打包一下phar檔,來取代之前money專案的phpunit.phar,因為money的phpunit.phar裡面沒有selenium或是facebook/webdriver

PHP的phar可以想像成java的.jar檔,把一堆lib打包起來,讓我們來直接看看怎麼打包跟使用,比如說我們要來打包phpunit + phpunit/selenium + facebook/webdriver

打包程式碼大概有幾個步驟

(1)先設定php.ini

php.ini
[Phar]
; http://php.net/phar.readonly
phar.readonly = Off

(2)準備好要打包的目錄跟程式

composer.json
{
    "require": {
        "phpunit/phpunit": "~4.5",
        "phpunit/phpunit-selenium": "~1.4",
        "facebook/webdriver": "~1.0"
    }
}

目錄大概長這樣

selenium-php
│   phar-builder.php 
│
└───vendor
    |   autoload.php
    |   bootstrap.php
    |
    ├───bin
    │   │   phpunit
    │   │   ...
    │
    ├───php-webdriver-community
    │   │   ...
    │
    └───phpunit
    |   |
    |   |───phpunit-selenium
    |   |   |   ...
    |   |   
    |   |───phpunit
    |   |   |   ...

(3)先寫好一支執行打包程式,裡面內容包含要寫
你要打包什麼目錄?
打包出來的檔名要叫什麼?
執行這個phar檔的時候,第一支要執行裡面的什麼程式?
要使用什麼壓縮技術?
我們姑且把這支程式命名成phar_builder.php
打包出來的phar檔就叫做phpunit_facebook_webdriver.phar

phar_builder.php
<?php // phar_builder.php

  $stub = <<<EOF
  #!/usr/bin/env php
  <?php
    Phar::mapPhar('phpunit_facebook_webdriver.phar');
    require 'phar://phpunit_facebook_webdriver.phar/bootstrap.php';
    __HALT_COMPILER();
  ?>
EOF;

  $phar = new Phar('phpunit_facebook_webdriver.phar');
  $phar->setStub($stub);
  $phar->buildFromDirectory('vendor');
  $phar->compressFiles(Phar::BZ2);
  $phar->stopBuffering();
?>

我們先來看怎麼使用打包的語法
指定名稱的時候,一定要是.phar結尾

<?php
  $phar = new Phar('phpunit_facebook_webdriver.phar');
?>

setStub方法,就是直接執行phar檔的時候,如果沒有特別指定預設執行檔,他就會預設執行phar檔裡面的index.php

<?php
  $phar->setStub($stub);
?>

打包vendor目錄下的所有檔案

<?php
  $phar->buildFromDirectory('vendor');
?>

採用GZ壓縮, mac os若不能執行, 可改用Phar::BZ2

<?php
  $phar->compressFiles(Phar::GZ);
?>

處理完畢,寫入檔案

<?php
  $phar->stopBuffering();
?>

(4)寫好phar裡面bootstrap.php
如果沒有做額外指定,phar會優先執行index.php這是預設的
你可以在裡面寫autoload,或者你也可以直接用composer的autoload
這邊的作法是先載入composer的autoload再執行phpunit的PHPUnit_TextUI_Command::main()

<?php
require_once 'autoload.php';
define('PHPUnit_MAIN_METHOD', 'PHPUnit_TextUI_Command::main');
PHPUnit_TextUI_Command::main();
?>

(5)執行寫好的phar_builder.php

$ php phar_builder.php

這樣就會產生出phpunit_facebook_webdriver.phar這個檔案
可以直接使用

$ php build/tools/phpunit_facebook_webdriver.phar --configuration build/phpunit.xml

你想要require phar檔,你也可以這樣搞

require_once 'phar://' . dirname(__FILE__) . '/../phpunit_facebook_webdriver.phar/autoload.php';

再跟大家介紹Apache Ant

它是一個工具軟體,剛剛提到,你可以透過Ant來執行一連串自動化的事情,使用Apapche Ant所提供的xml,預設叫做build.xml,來告訴Ant該做什麼事,而Jenkins只是呼叫它而已。這邊有滿詳細的使用說明

我們來看build.xml

build.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="Money" default="build">
    <!--build工作項依賴[prepare,lint,phploc-ci,phpcs-ci,phpunit,-check-failure]-->
    <!--依賴的工作項如果都成功,build這個工作項才算成功-->
    <target name="build" depends="prepare,lint,phploc-ci,phpcs-ci,phpunit,-check-failure"/>

    <!--執行清除log-->
    <target name="clean" unless="clean.done" description="Cleanup build artifacts">
        <delete dir="${basedir}/build/coverage"/>
        <delete dir="${basedir}/build/logs"/>
        <property name="clean.done" value="true"/>
    </target>

    <!--執行準備事項-->
    <target name="prepare" unless="prepare.done" depends="clean" description="Prepare for build">
        <mkdir dir="${basedir}/build/coverage"/>
        <mkdir dir="${basedir}/build/logs"/>
        <property name="prepare.done" value="true"/>
    </target>

    <!--從這邊開始,都是針對PHP程式碼做分析-->
    <target name="lint" depends="prepare">
        <apply executable="php" failonerror="true" taskname="lint">
            <arg value="-l"/>

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

    <target name="phploc" description="Measure project size using PHPLOC">
        <exec executable="${basedir}/build/tools/phploc.phar" taskname="phploc">
            <arg value="--count-tests"/>
            <arg path="${basedir}/app/protected/controllers"/>
            <arg path="${basedir}/app/protected/models"/>
        </exec>
    </target>

    <target name="phploc-ci" depends="prepare"
            description="Measure project size using PHPLOC and log result in CSV and XML format">
        <exec executable="${basedir}/build/tools/phploc.phar" taskname="phploc">
            <arg value="--count-tests"/>
            <arg value="--log-csv"/>
            <arg path="${basedir}/build/logs/phploc.csv"/>
            <arg value="--log-xml"/>
            <arg path="${basedir}/build/logs/phploc.xml"/>
            <arg path="${basedir}/app/protected/controllers"/>
            <arg path="${basedir}/app/protected/models"/>
        </exec>
    </target>

    <target name="phpcs"
            description="Find coding standard violations using PHP_CodeSniffer and print result in text format">
        <exec executable="${basedir}/build/tools/phpcs.phar" taskname="phpcs">
            <arg value="--standard=PSR2"/>
            <arg value="--extensions=php"/>
            <arg value="--ignore=autoload.php"/>
            <arg path="${basedir}/app/protected/controllers"/>
            <arg path="${basedir}/app/protected/models"/>
        </exec>
    </target>

    <target name="phpcs-ci" depends="prepare"
            description="Find coding standard violations using PHP_CodeSniffer and log result in XML format">
        <exec executable="${basedir}/build/tools/phpcs.phar" output="/dev/null" taskname="phpcs">
            <arg value="--report=checkstyle"/>
            <arg value="--report-file=${basedir}/build/logs/checkstyle.xml"/>
            <arg value="--standard=PSR2"/>
            <arg value="--extensions=php"/>
            <arg value="--ignore=autoload.php"/>
            <arg path="${basedir}/app/protected/controllers"/>
            <arg path="${basedir}/app/protected/models"/>
        </exec>
    </target>

    <!--執行phpunit test-->
    <target name="phpunit" depends="prepare" description="Run unit tests with PHPUnit">
        <exec executable="php" resultproperty="result.phpunit" taskname="phpunit">
            <arg path="${basedir}/build/tools/phpunit_facebook_webdriver.phar"/>
            <arg value="--configuration"/>
            <arg path="${basedir}/build/phpunit.xml"/>
        </exec>
    </target>

    <!--檢查是否錯誤-->
    <target name="-check-failure">
        <fail message="PHPUnit did not finish successfully">
            <condition>
                <not>
                    <equals arg1="${result.phpunit}" arg2="0"/>
                </not>
            </condition>
        </fail>
    </target>

    <!--從這邊開始都是佈署所需要的設定檔-->
    <!--設定相關屬性-->
    <property name="apptarfile" value="phptheday23.tar.gz" />

    <fileset id="phptheday23.tar.gz" dir=".">
        <include name="app/**"/>
        <include name="yii/**"/>
    </fileset>

    <!--這邊做的事情是,打包程式碼,以利遠端佈署使用-->
    <target name="tar" description="Create tar file for release">
        <delete file="${apptarfile}" failonerror="false"/>
        <tar destfile="${apptarfile}" compression="gzip">
            <!-- refid會參照上面設定的fileset -->
            <fileset refid="phptheday23.tar.gz"/>
        </tar>
    </target>
</project>

東西真多...我們來看其中兩個例子


把上面紅框的字串起來,其實就是長這樣

php ${basedir}/build/tools/phpunit_facebook_webdriver.phar --configuration ${basedir}/build/phpunit.xml

稍微解說一下:
target:可以把它想成是一個工作的目標,它這個target可以幫你達到什麼目的,裡面的屬性可以設定,它的名稱、描述、以及它要依賴哪一個target
exec:把它想成執行即可
arg:輸入command line的時候,都會需要輸入什麼參數,或者是要在哪個目錄執行


解說一下:
我們可以先自定義一個屬性,apptarfile,如紅框(1),接著我們可以再定義一個fileset,如紅框(4),類似於一個檔案空間,接著當tar這個target一執行,它會先刪除phptheday23.tar.gz(這是舊的release檔案),如紅框(3),接著再利用tar這個元素告訴Ant,要做打包的動作,打包的內容參照apptarfile這個屬性,我要打包app跟yii目錄裡所有的東西。

我們來看phpunit.xml

phpunit.xml
<phpunit bootstrap="../app/protected/tests/bootstrap.php"
        colors="false"
        convertErrorsToExceptions="true"
        convertNoticesToExceptions="true"
        convertWarningsToExceptions="true"
        stopOnFailure="false">

    <testsuite name="PHPUnit">
        <directory suffix="Test.php">../app/protected/tests/unit</directory>
        <directory suffix="Test.php">../app/protected/tests/functional</directory>
        <!--<directory suffix="Test.php">../app/protected/tests/outside</directory>-->
    </testsuite>

    <!--log要先設定好對應的位置,這樣Jenkins才能抓到log做分析-->
    <logging>
        <log type="coverage-html" target="coverage"/>
        <log type="coverage-clover" target="logs/clover.xml"/>
        <log type="coverage-crap4j" target="logs/crap4j.xml"/>
        <log type="coverage-xml" target="logs/coverage"/>
        <log type="junit" target="logs/junit.xml"/>
    </logging>
</phpunit>

要記得log匯出的地方要讓Jenkis抓得到
比如說junit的指定目錄在phpunit.xml所在的目錄底下的logs/junit.xml
簡單來說,如果phpunit.xml在build目錄,那log的目的地就在build/logs/junit.xml
這個要跟Jenkins的路徑要對起來

<log type="junit" target="logs/junit.xml"/>


再來,真正得來建立Jenkins的排程專案

一樣,使用php-template,並且做微調設定,也把git設定上去

(1).把專案名稱設定成phptheday23

(2).https://github.com/luckily/phptheday23.git

(3).可以設定多久輪詢一次github,來達到真正的自動化,只要有人commit,一分鐘內就會馬上執行建置流程了

(4).把Publish HTML reports的Option都勾選起來

(5).另外jdepend的項目,也需要拿掉,因為我的build.xml裡面沒有做jdepend(pdepend)的相關設定

(6).加入執行Shell我們除了設定要用Ant執行自動化的事件之外,我們還需要做權限上的設定,否則nginx會無法執行,記得執行Shell必須放在叫用Ant前面

設定workspace的權限,讓nginx可以執行

sudo chmod 775 -R ${WORKSPACE}
sudo chgrp -R www-data ${WORKSPACE}



設定Nginx的config

設定完專案的同時,我們要把nginx的公開目錄設定在Jenkins的workspace,Jenkins都會把code拉到workspace,這樣我們稍後才能用selenum server來執行CI Server本身自己的test,

設定一下nginx的config
$ sudo vim /etc/nginx/sites-available/default
/etc/nginx/sites-available/default
root /var/lib/jenkins/jobs/phptheday23/workspace;
location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php5-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

重啟一下Nginx

$ sudo service nginx restart

我們來測試一下http://yourhostname/app/index.php

來讓跑看看Jenkins自動化測試


設定自動佈署

(1)設定一下遠端Server的資訊



(2)新增、設定Jenkins的排程專案

記得遠端Server也要設定把jenkins加進sudoer,才不會有問題哦!
設定完成之後,儲存。








這邊有設定檔上的小抄

第二組設定
# Source files

phptheday23.tar.gz
 
# Remote directory  

phptheday23
 
# Exec command (小心別打錯了...)

$ sudo rm -rf ./*
第三組設定
# Remote directory  

phptheday23
 
# Exec command

$ tar -zxf /usr/share/nginx/html/phptheday23/phptheday23.tar.gz -C /usr/share/nginx/html/phptheday23
$ sudo rm /usr/share/nginx/html/phptheday23/phptheday23.tar.gz
$ sudo chown -R nginx /usr/share/nginx/html/phptheday23
$ sudo chgrp -R nginx /usr/share/nginx/html/phptheday23
$ sudo chmod -R 775 /usr/share/nginx/html/phptheday23
(3)把自動佈署跟測試串連起來,讓Jenkins測試完之後,接著再執行佈署

先回到phptheday23專案,再按照列下步驟設定



(4)都ok,我們來看網站有沒有真的改變

變化前

測試+佈署中...請稍後

變化後


終於完成CI Server建置了

架CI Server好累,去用Travis好了 ??(誤...XD

← mysql prepared statements