Nerrvana в работе – сборка приложения и Jenkins (часть 2)

Using Nerrvana - deployment & Jenkins (part 2)

Создание Jenkins job – подробности
сборки приложения у нас – установка
приложения для тестирования

Part 1 – Nerrvana в работе – как это делается у нас
Part 2 – Nerrvana в работе – SVN втыкается в Jenkins
Part 3 – Nerrvana в работе – сборка приложения и Jenkins (часть 1)
Part 4 – Nerrvana в работе – сборка приложения и Jenkins (часть 2) – этот пост

В предыдущем посте мы закончили конфигурирование Jenkins и теперь можем создать задачу по тестированию нашего приложения.

Открываем страницу http://your_jenkins_host/view/All/newJob и вводим название job. Поскольку наше приложение называется Answers, то мы используем его.

На практике удобно добавить информацию о версии. Например, job ‘Answers TRUNK’ тестирует TRUNK, job ‘Answers Release 1.4′ тестирует Release 1.4.

New Jenkins job

Далее в секции ‘Advanced Project Options’ нажимаем кнопку ‘Advanced …’ и выставляем ‘Quiet period’ в 0 секунд. Мы знаем, что многие делают build несколько раз в день. В этом случае параметры запуска Jenkins job будут иными. Мы же делаем build под каждый коммит и потому нам нет смысла ждать 5 секунд. Помощь в Jenkins к этому пункту очень хорошо описывает практические случаи, когда эта опция нужна.

New Jenkins job - Advanced project options

Теперь переходим к секции ‘Source Code Management’, выбираем SVN, который мы используем.

New Jenkins job - Source Code Management

Jenkins попросит ввести детали пользователя SVN. Выбираем ‘Check-out strategy’ – ‘Always check-out a fresh copy’.

New Jenkins job - Check out strategy

Следующая секция – ‘Build Triggers’. Используем ‘Poll SCM’. В нашем случае push-триггером, который советует использовать справка к этому пункту, является SVN-хук, который мы добавили ранее. Если вы откроете справку к опции ‘Build periodically’, вы увидите следующее замечание: “When people first start continuous integration, they are often so used to the idea of regularly scheduled builds like nightly/weekly that they use this feature. However, the point of continuous integration is to start a build as soon as a change is made, to provide a quick feedback to the change.” Как раз поэтому наши builds запускаются сразу при изменении кода проекта.

New Jenkins job - Build Triggers

Далее пропускаем секцию ‘Build Environment’ и переходим к ‘Build’. Нажимаем кнопку ‘Add build step’ и выбираем опцию ‘Invoke Ant’.

New Jenkins job - Add Build Step

Как вы видите, конфигурационный файл Ant находится тоже в SVN. После извлечения кода на предыдущем шаге он оказывается в нужном месте в нужное время (хочется ему в этом позавидовать). Задача этого шага в том, чтобы подготовить код, извлеченный из системы контроля версий, к передаче на deployment хост для дальнейшей установки приложения на нём. Мы хотим по возможности сделать максимум операций на jenkins хосте.

New Jenkins job - Invoke Ant

Далее я расскажу, чем мы озадачили Ant. Вот так выглядит наш build.xml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?xml version="1.0" encoding="UTF-8"?>
<project name="${env.JOB_NAME}" default="build" basedir=".">
    <property environment="env"/>
    <property name="tests" value="${env.WORKSPACE}/tests"/>
    <property name="ci" value="${env.WORKSPACE}/ci"/>
    <property name="code" value="${env.WORKSPACE}/code"/>
 
    <target name="make_settings">
        <exec executable="sed" output="${code}/config/Settings.class.php.mysql">
            <arg line="-f ${ci}/deployment/config/mysql/settings-mysql.sed ${code}/config/Settings.class.template.php" />
        </exec>
    </target>
 
    <target name="make_sql">
        <exec executable="sed" output="${code}/install/mysql.sql">
            <arg line="-f ${ci}/deployment/config/mysql/install-mysql.sed ${code}/install/mysql.sql" />
        </exec>
    </target>
 
    <target name="move_files" depends="make_settings,make_sql">
        <!-- Move authentication files to WAUT root -->
       <move file="${ci}/deployment/misc/login.php" tofile="${code}/login.php" />
        <move file="${ci}/deployment/misc/logout.php" tofile="${code}/logout.php" />
 
        <!-- Move MySQL db creation files to install folder -->
        <move file="${ci}/deployment/config/mysql/crt-mysql.sql" tofile="${code}/install/crt-mysql.sql" />
 
        <!-- Move publish script to the workspace root, so it will be beside prj.zip -->
       <move file="${ci}/deployment/publish-over-ssh/publish.sh" tofile="${env.WORKSPACE}/publish.sh" />
    </target>
 
    <target name="save_commit_info" depends="move_files">
        <exec dir="${env.WORKSPACE}" executable="svn" >
                    <arg line="--username jenkins --password 123456 upgrade" />
        </exec>    
      <exec dir="${env.WORKSPACE}" executable="svn"  output="${env.WORKSPACE}/version.txt">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
      <exec dir="${env.WORKSPACE}" executable="svn">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
 
    </target>   
 
    <target name="svn" depends="save_commit_info">
       <java dir="." jar="${ci}/deployment/scm-decorator.jar" fork="true" failonerror="true">
         <arg value="svn"/>
         <arg value="${env.WORKSPACE}/version.txt"/> 
       </java>       
    </target>
 
    <target name="zip" depends="svn" description="Compress project files">
        <jar destfile="${env.WORKSPACE}/prj.zip"
            basedir="${env.WORKSPACE}/code"
            excludes=".svn" />
    </target>
 
   <target name="build" depends="zip" />
</project>

Все остальные операции выполняются на стороне deployment хоста после передачи результатов этого шага с помощью ‘Send files or execute commands over SSH’.

Надеюсь, мои пояснения помогут вам создать Ant build ещё лучше и ещё быстрее.

Прежде всего, хочу сказать, что если вы запутаетесь с путями – вставьте в ваш Ант файл следующий target и тогда в логе build-a вы увидите какими путями оперирует Jenkins.

<target name="test">
    <echo message="Jenkins workspace: ${env.WORKSPACE}"/>
    <echo message="Job directory: ${env.WORKSPACE}/../../${env.JOB_NAME}"/>
    <echo message="Build data: ${env.WORKSPACE}/../../${env.JOB_NAME}/builds/${env.BUILD_ID}"/>
</target>

В строках 4-6 мы даём более краткие имена (aliases),чтобы дальше было удобнее прописывать пути.

<property name="tests" value="${env.WORKSPACE}/tests"/>
    <property name="ci" value="${env.WORKSPACE}/ci"/>
    <property name="code" value="${env.WORKSPACE}/code"/>

Далее (строки 8-12) наша задача – создать конфигурационный файл приложения. Как мы писали ранее, наше приложение – загружаемое и инсталлируется с помощью поставляемого с ним инсталляционного скрипта. Здесь же мы просто используем sed для превращения шаблона Settings.class.template в нужный нам конфигурационный файл Settings.class.php.

<target name="make_settings">
        <exec executable="sed" output="${code}/config/Settings.class.php.mysql">
            <arg line="-f ${ci}/deployment/config/mysql/settings-mysql.sed ${code}/config/Settings.class.template.php" />
        </exec>
    </target>

Я не буду приводить файлы целиком, а просто покажу несколько строк из файла заготовки – Settings.class.template, несколько строк settings-mysql.sed файла и те же строки во вновь созданном конфигурационном файле Settings.class.php.

// Database IP or hostname (use colon, if runs on non-standard port: 127.0.0.1:563)
    const DB_HOST = "";
 
    // Username to connect to DB
    const DB_USER = "";
s/const DB_USER = "";/const DB_USER = "ANSWERS_DB_USER";/g
s/const DB_HOST = "";/const DB_HOST = "localhost";/g
// Database IP or hostname (use colon, if runs on non-standard port: 127.0.0.1:563)
    const DB_HOST = "localhost";
 
    // Username to connect to DB
    const DB_USER = "ANSWERS_DB_USER";

Вы видите, что в именах файлов и путях присутствует ‘mysql’ Это потому, что мы тестируем наше приложение ещё и с PostgreSQL. Я убрал из описания всё, что касается PostgreSQL – как и обещал, приведу реальные конфиги в самом конце цикла. Сам не люблю, когда объясняя мне что-то новое, вываливают кучу не важной информации, от которой всё только запутывается.

В результате этого шага мы получили конфигурационный файл приложения для работы с MySQL – Settings.class.php.mysql, который лежит рядышком с Settings.class.template.php в папке ${code}/config/.

В строках 14-18 мы создаём необходимый SQL файл, который, будучи запущенным на deployment хосте, пересоздаст MySQL базу данных приложения.

<target name="make_sql">
        <exec executable="sed" output="${code}/install/mysql.sql">
            <arg line="-f ${ci}/deployment/config/mysql/install-mysql.sed ${code}/install/mysql.sql" />
        </exec>
    </target>

Опять же, инсталлятор Answers использует заранее приготовленный файл mysql.sql, в котором в процессе установки на основании введённых пользователей данных заменяются, например, имена таблиц. Но мы можем его использовать практически без изменений. Изменения, которые необходимы, делаем с помощью sed. В данном случае модифицируем оригинальный файл ${code}/install/mysql.sql.

s/USERS_TABLE/USERS_VIEW_NAME/g
s/USE ANSWERS_DATABASE/USE answers/g

Идём далее – target “move_files” (строки 20-30). Как вы, наверное, помните, наше приложение – встраиваемое и не имеет своей собственной страницы авторизации. Однако для тестирования нам всё равно нужны страницы login и logout, поэтому нам пришлось их создать специально для тестирования. Лежат они в ${ci}/deployment/misc/, и теперь самое время передвинуть их в корень приложения – ${code}/, так как этими страницами будут пользоваться Selenium тесты (строки 21-23).

<target name="move_files" depends="make_settings,make_sql">
        <!-- Move authentication files to WAUT root -->
       <move file="${ci}/deployment/misc/login.php" tofile="${code}/login.php" />
        <move file="${ci}/deployment/misc/logout.php" tofile="${code}/logout.php" />
 
        <!-- Move MySQL db creation files to install folder -->
        <move file="${ci}/deployment/config/mysql/crt-mysql.sql" tofile="${code}/install/crt-mysql.sql" />
 
        <!-- Move publish script to the workspace root, so it will be beside prj.zip -->
       <move file="${ci}/deployment/publish-over-ssh/publish.sh" tofile="${env.WORKSPACE}/publish.sh" />
    </target>

В строках 25-26 мы перемещаем ещё один SQL файл – crt-mysql.sql, из ${ci}/deployment/config/mysql/ в ${code}/install/, так как мы отправляем на deployment хост только содержимое ${code}/.

Что же находится в файле crt-mysql.sql? Приложения встраиваемое, поэтому создаёт свои таблицы в базе данных основного приложения. Чтобы смоделировать эту ситуацию, нам нужно (пере)создать базу этого основного приложения, чтобы потом после этого мы смогли бы выполнить файл mysql.sql. То есть задача скрипта – создание базы “основного” приложения и наполнение нужными Answers данными. Вот так он выглядит без купюр:

DROP DATABASE answers;
CREATE DATABASE answers;
use answers;
 
DROP TABLE if exists USERS_SOURCE_TABLE;
CREATE TABLE `USERS_SOURCE_TABLE` (
  USERS_ID_COLUMN int(11),
  username varchar(50),
  USERS_DISPLAY_NAME_COLUMN varchar(50),
  USERS_EMAIL_COLUMN varchar(50),
  CONSTRAINT USERS_SOURCE_TABLE_pkey PRIMARY KEY (USERS_ID_COLUMN)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
INSERT INTO USERS_SOURCE_TABLE VALUES(1,'answ_test_admin@dslabs.lan','Arnie - Admin','answ_test_admin@dslabs.lan');
INSERT INTO USERS_SOURCE_TABLE VALUES(2,'answ_test_user@dslabs.lan','Ivan Danko - Captain','answ_test_user@dslabs.lan');
INSERT INTO USERS_SOURCE_TABLE VALUES(3,'answ_test_staff@dslabs.lan','Lee - Guru(Staff)','answ_test_staff@dslabs.lan');
 
DROP USER 'ANSWERS_DB_USER'@localhost;
FLUSH PRIVILEGES;

И в заключение этой части в строках 28-29, мы переносим скрипт publish.sh в корень WORKSPACE. Этот скрипт знает, что делать з запакованным в zip /code, который мы подготовили. О его содержимом мы поговорим, когда будем описывать шаг ‘Send files or execute commands over SSH’.

В строках 32-43 мы сохраняем в текстовый файл информацию о коммите, который будем тестировать.

<target name="save_commit_info" depends="move_files">
        <exec dir="${env.WORKSPACE}" executable="svn" >
                    <arg line="--username jenkins --password 123456 upgrade" />
        </exec>    
      <exec dir="${env.WORKSPACE}" executable="svn"  output="${env.WORKSPACE}/version.txt">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
      <exec dir="${env.WORKSPACE}" executable="svn">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>                  
    </target>

В строках 33-35 у меня находится решение проблемы, когда SVN-сервер имеет более свежую версию, чем поддерживается SVN-плагином для Jenkins. В этом случае команда ‘svn info’ выдаёт ошибку о несовпадении версий и просит сделать upgrade рабочей копии, что я и делаю до выполнения ‘svn info’. Строки 39-41 только для того, чтобы та же информация попала и в консольный лог Jenkins. Эту информацию мы преобразуем на следующем шаге в более приятный формат и добавим позже в описание Nerrvana test run.

Теперь мы используем написанную нами и доступную на GitHub утилитку, которая получает на входе такой файл и …

#cat version.txt
------------------------------------------------------------------------
r69 | igork | 2012-10-03 07:38:38 +0000 (Wed, 03 Oct 2012) | 1 line
 
Fixed bug #342
------------------------------------------------------------------------

… преобразовывает его в такой.

#cat version.txt
Revision: 69
Committer: igork
Date: 2012-10-03 07:38:38 +0000 (Wed, 03 Oct 2012)
Fixed bug #342

Вы можете расширить наш код, добавив поддержку системы контроля версий, которую используете вы, если она окажется вам полезной.

Вот так это делается:

<target name="svn" depends="save_commit_info">
       <java dir="." jar="${ci}/deployment/scm-decorator.jar" fork="true" failonerror="true">
         <arg value="svn"/>
         <arg value="${env.WORKSPACE}/version.txt"/> 
       </java>       
    </target>

Как вы видите, специально с учётом возможных расширений предусмотрен параметр value=”svn”.

И наконец мы zip-уем папку /code/ в строках 51-55 в файл prj.zip.

<target name="zip" depends="svn" description="Compress project files">
        <jar destfile="${env.WORKSPACE}/prj.zip"
            basedir="${env.WORKSPACE}/code"
            excludes=".svn" />
    </target>

Переходим к следуюшему шагу build-a (и на сегодня последнему) – передаче приготовленных файлов и завершению deploymenta-а

New Jenkins job - Send files or execute commands over SSH

Вы видите, что мы просим передать на deployment хост (определяется первым параметром “SSH Server”, который мы создали, когда конфигурировали Jenkins) два файла prj.zip и publish.sh, а также выполнить после передачи:

chmod +x publish.sh
./publish.sh

Теперь самое время посмотреть, что же делает publish.sh скрипт.

#!/bin/sh
 
WWW_MYSQL_DIR='/var/www/answers/answers_mysql'
DB_NAME='answers'
 
rm -Rf $WWW_MYSQL_DIR/*
unzip -o prj.zip -d $WWW_MYSQL_DIR
mv $WWW_MYSQL_DIR/config/Settings.class.php.mysql $WWW_MYSQL_DIR/config/Settings.class.php
chmod -R 777 $WWW_MYSQL_DIR
mysql -v -u root  < $WWW_MYSQL_DIR/install/crt-mysql.sql
mysql -v -u root  < $WWW_MYSQL_DIR/install/mysql.sql $DB_NAME

Чистит папку веб серера:

rm -Rf $WWW_MYSQL_DIR/*

Распаковывает туда Answers

unzip -o prj.zip -d $WWW_MYSQL_DIR

Переименовывает подготовленный конфигурационный файл для MySQL

mv $WWW_MYSQL_DIR/config/Settings.class.php.mysql $WWW_MYSQL_DIR/config/Settings.class.php

Устанавливает права на файлы нашего приложения (мы могли бы быть здесь немножечко консервативнее)

chmod -R 777 $WWW_MYSQL_DIR

Создаёт ‘основное’ приложение (-v добавлено для verbose вывода работы SQL скрипта в консольный лог Jenkins):

mysql -v -u root < $WWW_MYSQL_DIR/install/crt-mysql.sql

Интегрирует в него Answers:

mysql -v -u root < $WWW_MYSQL_DIR/install/mysql.sql $DB_NAME

Всё готово! Делаем пробный коммит и смотрим консольные логи Jenkins, пока не добьёмся работающего приложения на http://deployment_host/answers_mysql.

Как вы видите, все файлы, используемые Jenkins-ом, находятся в системе контроля версий. Это позволяет не использовать custom workspaces в Jenkins, которые потребовали бы предварительной конфигурации и поддержки в актуальном состоянии (вначале у нас было именно так и это было неудобно). Все файлы попадают на свои места после извлечения кода Jenkins-ом. Также при таком подходе мы можем использовать переменные окружения Jenkins в файлах.

Под спойлером пример консольного лога выполненного build-а, с моими комментариями. Вырезаны и помечены как вырезанные логи, соответствующие одному шагу (например, ‘svn checkout’).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
Started by an SCM change
Building in workspace /var/lib/jenkins/jobs/Answers_deployment_only/workspace
Cleaning local Directory .
Checking out http://192.168.3.97/repos/answers/trunk at revision 140
A         tests
------ Output removed - comment by Igor --------------------------------- 
AU        ci/deployment/scm-decorator.jar
A         ci/deployment/build.xml
A         ci/toolchain
A         ci/toolchain/build-toolchain.xml
At revision 140
[deployment] $ ant -file build.xml build
Buildfile: build.xml
 
make_settings:
 
make_sql:
 
move_files:
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace/code
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace/code/install
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace
 
save_commit_info:
     [exec] Upgraded '.'
------ Output removed - comment by Igor --------------------------------- 
     [exec] Upgraded 'ci/deployment/misc'
     [exec] Upgraded 'ci/toolchain'
     [exec] ------------------------------------------------------------------------
     [exec] r140 | igork | 2012-10-08 11:46:01 +0000 (Mon, 08 Oct 2012) | 1 line
     [exec] 
     [exec] Test 2345
     [exec] ------------------------------------------------------------------------
 
svn:
 
zip:
      [jar] Building jar: /var/lib/jenkins/jobs/Answers_deployment_only/workspace/prj.zip
 
build:
 
BUILD SUCCESSFUL
Total time: 0 seconds
SSH: Connecting from host [mantis.deepshiftlabs.com]
SSH: Connecting with configuration [Answers test server] ...
SSH: EXEC: STDOUT/STDERR from command [chmod +x publish.sh
./publish.sh] ...
Archive:  prj.zip<div style="float: left;"><img style="padding: 5px 20px 10px 0;" title="Using Nerrvana - deployment &amp; Jenkins (part 2)" src="http://www.deepshiftlabs.com/sel_blog/wp-content/uploads/2012/08/deployment_2_300.png" alt="Using Nerrvana - deployment &amp; Jenkins (part 2)" /></div>
[lang_ru]
<p style="text-align: center;"><em>Создание Jenkins job - подробности</br>сборки приложения у нас - установка</br>приложения для тестирования</em></p>
 
В <a href="http://www.deepshiftlabs.com/sel_blog/?p=2255&lang=ru" target="_blank">предыдущем посте</a> мы закончили конфигурирование Jenkins и теперь можем создать задачу по тестированию нашего приложения.
 
Открываем страницу http://your_jenkins_host/view/All/newJob и вводим название job. Поскольку наше приложение называется <a href="http://answers.starty.co" target="_blank">Answers</a>, то мы используем его.
 
На практике удобно добавить информацию о версии. Например, job 'Answers TRUNK' тестирует TRUNK, job 'Answers Release 1.4' тестирует Release 1.4.
[/lang_ru][lang_en-us]
<p style="text-align: center;"><em>What is a web application deployment - Jenkins installation and configuration - preparing Jenkins for deployment</em></p>
Let’s talk about deployment. In the most general case, it is a transformation of the source code retrieved from a version control system into a running application. We do not use Jenkins to update our live sites and services yet, but we assume that the deployment for testing is not very different from deployment for upgrade or install. In the latter case, you need to take care of users, informing them in advance about the service downtime, and to give attention to security - access rights to files, the possibility of a rollback automatically in case of failure. These problems do not interest us at the moment anyway. We need to get the application running on an internal server which can be accessed by Selenium tests. Each application will have a slightly different deployment, and we will describe how we deploy, with some simplifications. In the last post of this mini-series, we will show our real configuration, but for now this information will be excessive.
[/lang_en-us]<!--more-->[lang_ru]
 
<img class="aligncenter" style="-ms-interpolation-mode: bicubic;" src="http://www.deepshiftlabs.com/sel_blog/wp-content/uploads/2012/10/build_1.png" alt="New Jenkins job" title="New Jenkins job" />
 
Далее в секции 'Advanced Project Options' нажимаем кнопку 'Advanced ...' и выставляем 'Quiet period' в 0 секунд. Мы знаем, что многие делают build несколько раз в день. В этом случае параметры запуска Jenkins job будут иными. Мы же делаем build под каждый коммит и потому нам нет смысла ждать 5 секунд. Помощь в Jenkins к этому пункту очень хорошо описывает практические случаи, когда эта опция нужна.
 
<img class="aligncenter" style="-ms-interpolation-mode: bicubic;" src="http://www.deepshiftlabs.com/sel_blog/wp-content/uploads/2012/10/build_advanced_1_5.png" alt="New Jenkins job - Advanced project options" title="New Jenkins job - Advanced project options" />
 
Теперь переходим к секции 'Source Code Management', выбираем SVN, который мы используем.
 
<img class="aligncenter" style="-ms-interpolation-mode: bicubic;" src="http://www.deepshiftlabs.com/sel_blog/wp-content/uploads/2012/10/build_svn_1.png" alt="New Jenkins job - Source Code Management" title="New Jenkins job - Source Code Management" />
 
Jenkins попросит ввести детали пользователя SVN. Выбираем 'Check out strategy' - 'Always check out a fresh copy'.
 
<img class="aligncenter" style="-ms-interpolation-mode: bicubic;" src="http://www.deepshiftlabs.com/sel_blog/wp-content/uploads/2012/10/build_svn_2.png" alt="New Jenkins job - Check out strategy" title="New Jenkins job - Check out strategy" />
 
Следующая секция 'Build Triggers'. Используем 'Poll SCM'. В нашем случае 'push' trigger-ом, который советует использовать справка к этому пункту, является SVN хук, который мы <a href="http://www.deepshiftlabs.com/sel_blog/?p=2253&lang=ru" target="_blank">добавили ранее</a>. Если вы откроете справку к опции 'Build periodically', вы увидите следующее замечание: "When people first start continuous integration, they are often so used to the idea of regularly scheduled builds like nightly/weekly that they use this feature. However, the point of continuous integration is to start a build as soon as a change is made, to provide a quick feedback to the change." Как раз, поэтому наши builds запускаются сразу при изменении кода проекта.
 
<img class="aligncenter" style="-ms-interpolation-mode: bicubic;" src="http://www.deepshiftlabs.com/sel_blog/wp-content/uploads/2012/10/build_triggers_3.png" alt="New Jenkins job - Build Triggers" title="New Jenkins job - Build Triggers" />
 
Далее пропускаем секцию 'Build Environment' и переходим к 'Build'. Нажимаем кнопку 'Add build step' и выбираем опцию 'Invoke Ant'.
 
<img class="aligncenter" style="-ms-interpolation-mode: bicubic;" src="http://www.deepshiftlabs.com/sel_blog/wp-content/uploads/2012/10/build_step_4.png" alt="New Jenkins job - Add Build Step" title="New Jenkins job - Add Build Step" />
 
Как вы видите, конфигурационный файл Ant находится тоже в SVN. По завершении извлечении кода на предыдущем шаге, он оказывается в нужном месте в нужное время (хочется ему в этом позавидовать). Задача этого шага в том, чтобы подготовить код, извлеченный из системы контроля версий, к передаче на deployment хост для дальнейшей установки приложения на нём. Мы хотим по возможности сделать максимум операций на jenkins хосте.
 
<img class="aligncenter" style="-ms-interpolation-mode: bicubic;" src="http://www.deepshiftlabs.com/sel_blog/wp-content/uploads/2012/10/build_ant.png" alt="New Jenkins job - Invoke Ant" title="New Jenkins job - Invoke Ant" />
 
Далее я расскажу, чем мы озадачили Ant. Вот так выглядит наш build.xml. 
 
<pre lang="xml" line="1"><?xml version="1.0" encoding="UTF-8"?>
<project name="${env.JOB_NAME}" default="build" basedir=".">
    <property environment="env"/>
    <property name="tests" value="${env.WORKSPACE}/tests"/>
    <property name="ci" value="${env.WORKSPACE}/ci"/>
    <property name="code" value="${env.WORKSPACE}/code"/>
 
    <target name="make_settings">
        <exec executable="sed" output="${code}/config/Settings.class.php.mysql">
            <arg line="-f ${ci}/deployment/config/mysql/settings-mysql.sed ${code}/config/Settings.class.template.php" />
        </exec>
    </target>
 
    <target name="make_sql">
        <exec executable="sed" output="${code}/install/mysql.sql">
            <arg line="-f ${ci}/deployment/config/mysql/install-mysql.sed ${code}/install/mysql.sql" />
        </exec>
    </target>
 
    <target name="move_files" depends="make_settings,make_sql">
        <!-- Move authentication files to WAUT root -->
       <move file="${ci}/deployment/misc/login.php" tofile="${code}/login.php" />
        <move file="${ci}/deployment/misc/logout.php" tofile="${code}/logout.php" />
 
        <!-- Move MySQL db creation files to install folder -->
        <move file="${ci}/deployment/config/mysql/crt-mysql.sql" tofile="${code}/install/crt-mysql.sql" />
 
        <!-- Move publish script to the workspace root, so it will be beside prj.zip -->
       <move file="${ci}/deployment/publish-over-ssh/publish.sh" tofile="${env.WORKSPACE}/publish.sh" />
    </target>
 
    <target name="save_commit_info" depends="move_files">
        <exec dir="${env.WORKSPACE}" executable="svn" >
                    <arg line="--username jenkins --password 123456 upgrade" />
        </exec>    
      <exec dir="${env.WORKSPACE}" executable="svn"  output="${env.WORKSPACE}/version.txt">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
      <exec dir="${env.WORKSPACE}" executable="svn">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
 
    </target>   
 
    <target name="svn" depends="save_commit_info">
       <java dir="." jar="${ci}/deployment/scm-decorator.jar" fork="true" failonerror="true">
         <arg value="svn"/>
         <arg value="${env.WORKSPACE}/version.txt"/> 
       </java>       
    </target>
 
    <target name="zip" depends="svn" description="Compress project files">
        <jar destfile="${env.WORKSPACE}/prj.zip"
            basedir="${env.WORKSPACE}/code"
            excludes=".svn" />
    </target>
 
   <target name="build" depends="zip" />
</project>

Все остальные операции выполняются на стороне deployment хоста после передачи результатов этого шага с помощью ‘Send files or execute commands over SSH’.

Надеюсь, мои пояснения помогут вам создать Ant build ещё лучше и ещё быстрее.

Прежде всего, хочу сказать, что если вы запутаетесь с путями – вставьте в ваш Ант файл следующий target и тогда в логе build-a вы увидите какими путями оперирует Jenkins.

<target name="test">
    <echo message="Jenkins workspace: ${env.WORKSPACE}"/>
    <echo message="Job directory: ${env.WORKSPACE}/../../${env.JOB_NAME}"/>
    <echo message="Build data: ${env.WORKSPACE}/../../${env.JOB_NAME}/builds/${env.BUILD_ID}"/>
</target>

В строках 4-6 мы даём более краткие имена (aliases),чтобы дальше было удобнее прописывать пути.

<property name="tests" value="${env.WORKSPACE}/tests"/>
    <property name="ci" value="${env.WORKSPACE}/ci"/>
    <property name="code" value="${env.WORKSPACE}/code"/>

Далее (строки 8-12) наша задача создать конфигурационный файл приложения. Как мы писали ранее наше приложение, загружаемое и инсталлируется с помощью поставляемого с ним инсталляционного скрипта. Здесь же мы просто используем sed для превращения заготовки используемой инсталляционным скриптом Settings.class.template в нужный нам конфигурационный файл Settings.class.php.

<target name="make_settings">
        <exec executable="sed" output="${code}/config/Settings.class.php.mysql">
            <arg line="-f ${ci}/deployment/config/mysql/settings-mysql.sed ${code}/config/Settings.class.template.php" />
        </exec>
    </target>

Я не буду приводить файлы целиком, а просто покажу несколько строк из файла заготовки – Settings.class.template, несколько строк settings-mysql.sed файла и те же строки во вновь созданном конфигурационном файле Settings.class.php.

// Database IP or hostname (use colon, if runs on non-standard port: 127.0.0.1:563)
    const DB_HOST = "";
 
    // Username to connect to DB
    const DB_USER = "";
s/const DB_USER = "";/const DB_USER = "ANSWERS_DB_USER";/g
s/const DB_HOST = "";/const DB_HOST = "localhost";/g
// Database IP or hostname (use colon, if runs on non-standard port: 127.0.0.1:563)
    const DB_HOST = "localhost";
 
    // Username to connect to DB
    const DB_USER = "ANSWERS_DB_USER";

Вы видите, что в именах файлов и путях присутствует ‘mysql’ Это потому, что мы ещё тестируем наше приложение с PostgreSQL. Я убрал все, что касается PostgreSQL из описания и, как и обещал, приведу реальные конфиги в самом конце цикла. Сам не люблю, когда объясняя мне что-то новое, вываливают кучу не важной информации, от которой всё только запутывается.

В результате этого шага мы получили конфигурационный файл приложения для работы с MySQL – Settings.class.php.mysql, который лежит рядышком с Settings.class.template.php в папке ${code}/config/.

В строках 14-18 мы создаём необходимый SQL файл, который, будучи запущенным на deployment хосте, пересоздаст MySQL базу данных приложения.

<target name="make_sql">
        <exec executable="sed" output="${code}/install/mysql.sql">
            <arg line="-f ${ci}/deployment/config/mysql/install-mysql.sed ${code}/install/mysql.sql" />
        </exec>
    </target>

Опять же, инсталлятор Answers использует заранее приготовленный файл mysql.sql в котором в процессе установки, на основании введённых пользователей данных, заменяются, например, имена таблиц, но мы можем его использовать практически без изменений. Изменения, которые необходимы, делаем с помощью sed. В данном случае модифицируем оригинальный файл ${code}/install/mysql.sql.

s/USERS_TABLE/USERS_VIEW_NAME/g
s/USE ANSWERS_DATABASE/USE answers/g

Идём далее – target “move_files” (строки 20-30). Как вы наверное помните, наше приложение встраиваемое и не имеет своей собственной страницы авторизации. Однако для тестирования нам нугны страницы login и logout, которые мы и создали. Лежат они в ${ci}/deployment/misc/ и теперь самое время передвинуть их в корень приложения – ${code}/, так как этими страницами будут пользоваться Selenium тесты (строки 21-23).

<target name="move_files" depends="make_settings,make_sql">
        <!-- Move authentication files to WAUT root -->
       <move file="${ci}/deployment/misc/login.php" tofile="${code}/login.php" />
        <move file="${ci}/deployment/misc/logout.php" tofile="${code}/logout.php" />
 
        <!-- Move MySQL db creation files to install folder -->
        <move file="${ci}/deployment/config/mysql/crt-mysql.sql" tofile="${code}/install/crt-mysql.sql" />
 
        <!-- Move publish script to the workspace root, so it will be beside prj.zip -->
       <move file="${ci}/deployment/publish-over-ssh/publish.sh" tofile="${env.WORKSPACE}/publish.sh" />
    </target>

В строках 25-26 мы перемещаем ещё один SQL файл – crt-mysql.sql, из ${ci}/deployment/config/mysql/ в ${code}/install/, так как мы отправляем на deployment хост только содержимое ${code}/.

Что же находится в файле crt-mysql.sql? Здесь опять из за встраиваемости приложения, которое создаёт свои таблицы в базе данных вашего основного приложения ннам нужно как бы (пере)создать базу этого основного приложения, чтобы потом после этого мы смогли бы выполнить файл mysql.sql, который мы подготовили ранее. То есть роль скрипта – пересоздание базы, создание “основного” приложения и его пользователей и удаление пользователя базы, которого создал предыдущий deployment. Вот так он выглядит без купюр:

DROP DATABASE answers;
CREATE DATABASE answers;
use answers;
 
DROP TABLE if exists USERS_SOURCE_TABLE;
CREATE TABLE `USERS_SOURCE_TABLE` (
  USERS_ID_COLUMN int(11),
  username varchar(50),
  USERS_DISPLAY_NAME_COLUMN varchar(50),
  USERS_EMAIL_COLUMN varchar(50),
  CONSTRAINT USERS_SOURCE_TABLE_pkey PRIMARY KEY (USERS_ID_COLUMN)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
INSERT INTO USERS_SOURCE_TABLE VALUES(1,'answ_test_admin@dslabs.lan','Arnie - Admin','answ_test_admin@dslabs.lan');
INSERT INTO USERS_SOURCE_TABLE VALUES(2,'answ_test_user@dslabs.lan','Ivan Danko - Captain','answ_test_user@dslabs.lan');
INSERT INTO USERS_SOURCE_TABLE VALUES(3,'answ_test_staff@dslabs.lan','Lee - Guru(Staff)','answ_test_staff@dslabs.lan');
 
DROP USER 'ANSWERS_DB_USER'@localhost;
FLUSH PRIVILEGES;

И в заключение этой части в строках 28-29, мы переносим скрипт publish.sh в корень Jenkins workspace. Этот скрипт знает, что делать с запакованным в zip /code, который мы подготовили. О его содержимом мы поговорим, когда будем описывать шаг ‘Send files or execute commands over SSH’.

В строках 32-43 мы сохраняем информацию о коммите, который будем тестировать в текстовый файл.

<target name="save_commit_info" depends="move_files">
        <exec dir="${env.WORKSPACE}" executable="svn" >
                    <arg line="--username jenkins --password 123456 upgrade" />
        </exec>    
      <exec dir="${env.WORKSPACE}" executable="svn"  output="${env.WORKSPACE}/version.txt">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
      <exec dir="${env.WORKSPACE}" executable="svn">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>                  
    </target>

В строках 33-35 у меня находится решение проблемы, когда ваш SVN сервер работает на более высокой версии, чем поддерживается Jenkins SVN плагином. Так может случиться. В этом случае команда ‘svn info’ выдаёт ошибку о несовпадении версий и просит сделать upgrade рабочей копии, что я и делаю до выполнения ‘svn info’. Строки 39-41 только для того чтобы та же информация попала в консольный лог Jenkins. Эту информацию мы преобразуем на следующем шаге в более приятный формат и добавим позже в описание Nerrvana test run.

Теперь мы используем написанную нами и доступную на GitHub (link) утилитку, которая получает на входе такой файл и …

#cat version.txt
------------------------------------------------------------------------
r69 | igork | 2012-10-03 07:38:38 +0000 (Wed, 03 Oct 2012) | 1 line
 
Fixed bug #342
------------------------------------------------------------------------

… преобразовывает его в такой.

#cat version.txt
Revision: 69
Committer: igork
Date: 2012-10-03 07:38:38 +0000 (Wed, 03 Oct 2012)
Fixed bug #342

Вы можете расширить наш код, добавив поддержку системы контроля версий, которую используете вы, если она окажется вам полезной.

Вот так это делается:

<target name="svn" depends="save_commit_info">
       <java dir="." jar="${ci}/deployment/scm-decorator.jar" fork="true" failonerror="true">
         <arg value="svn"/>
         <arg value="${env.WORKSPACE}/version.txt"/> 
       </java>       
    </target>

Как вы видите специально с учётом возможных расширений предусмотрен параметр .

И наконец мы zip-уем папку /code/ в строках 51-55 в файл prj.zip.

<target name="zip" depends="svn" description="Compress project files">
        <jar destfile="${env.WORKSPACE}/prj.zip"
            basedir="${env.WORKSPACE}/code"
            excludes=".svn" />
    </target>

Переходим к следуюшему шагу build-a (и на сегодня последнему) – передаче приготовленных файлов и окончание deploymenta-а

New Jenkins job - Send files or execute commands over SSH

Вы видите, что мы просим передать на deployment хост (определяется первым параметром – “SSH Server” и мы создали его, когда конфигурировали Jenkins) два файла prj.zip и publish.sh, а также выполнить после передачи:

chmod +x publish.sh
./publish.sh

Теперь самое время посмотреть, что же делает publish.sh скрипт.

#!/bin/sh
 
WWW_MYSQL_DIR='/var/www/answers/answers_mysql'
DB_NAME='answers'
 
rm -Rf $WWW_MYSQL_DIR/*
unzip -o prj.zip -d $WWW_MYSQL_DIR
mv $WWW_MYSQL_DIR/config/Settings.class.php.mysql $WWW_MYSQL_DIR/config/Settings.class.php
chmod -R 777 $WWW_MYSQL_DIR
mysql -v -u root  < $WWW_MYSQL_DIR/install/crt-mysql.sql
mysql -v -u root  < $WWW_MYSQL_DIR/install/mysql.sql $DB_NAME

Чистит папку веб сервера:

rm -Rf $WWW_MYSQL_DIR/*

Распаковывает туда Answers:

unzip -o prj.zip -d $WWW_MYSQL_DIR

Переименовывает подготовленный конфигурационный файл для MySQL:

mv $WWW_MYSQL_DIR/config/Settings.class.php.mysql $WWW_MYSQL_DIR/config/Settings.class.php

Устанавливает права на файлы нашего приложения (мы могли бы быть здесь немножечко консервативнее):

chmod -R 777 $WWW_MYSQL_DIR

Создаёт ‘основное’ приложение (-v добавлено для verbose вывода работы SQL скрипта в консольный лог Jenkins):

mysql -v -u root < $WWW_MYSQL_DIR/install/crt-mysql.sql

Интегрирует в него Answers:

mysql -v -u root < $WWW_MYSQL_DIR/install/mysql.sql $DB_NAME

Всё готово! Делаем пробный коммит и смотрим консольные логи Jenkins пока не добьёмся работающего приложения на http://deployment_host/answers_mysql.

Как вы видите все файлы, используемые Jenkins-ом находятся в системе контроля версий. Это позволяет не использовать custom workspaces в Jenkins, которые потребовали бы (вначале у нас было именно так и это было неудобно) предварительной конфигурации и поддержки в актуальном состоянии. Все файлы попадают на свои места после извлечения кода Jenkins-ом. Также, при таком подходе, мы можем использовать переменные окружения Jenkins в файлах.

Под спойлером пример консольного лога выполненного build-а, с моими комментариями. Вырезаны и помечены, как вырезанные, логи соответствующие одному шагу (например ‘svn checkout’).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Started by an SCM change
Building in workspace /var/lib/jenkins/jobs/Answers_deployment_only/workspace
Cleaning local Directory .
Checking out http://192.168.3.97/repos/answers/trunk at revision 140
A         tests
------ Output removed - comment by Igor --------------------------------- 
AU        ci/deployment/scm-decorator.jar
A         ci/deployment/build.xml
A         ci/toolchain
A         ci/toolchain/build-toolchain.xml
At revision 140
[deployment] $ ant -file build.xml build
Buildfile: build.xml
 
make_settings:
 
make_sql:
 
move_files:
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace/code
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace/code/install
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace
 
save_commit_info:
     [exec] Upgraded '.'
------ Output removed - comment by Igor --------------------------------- 
     [exec] Upgraded 'ci/deployment/misc'
     [exec] Upgraded 'ci/toolchain'
     [exec] ------------------------------------------------------------------------
     [exec] r140 | igork | 2012-10-08 11:46:01 +0000 (Mon, 08 Oct 2012) | 1 line
     [exec] 
     [exec] Test 2345
     [exec] ------------------------------------------------------------------------
 
svn:
 
zip:
      [jar] Building jar: /var/lib/jenkins/jobs/Answers_deployment_only/workspace/prj.zip
 
build:
 
BUILD SUCCESSFUL
Total time: 0 seconds
SSH: Connecting from host [mantis.deepshiftlabs.com]
SSH: Connecting with configuration [Answers test server] ...
SSH: EXEC: STDOUT/STDERR from command [chmod +x publish.sh
./publish.sh] ...
Archive:  prj.zip
   creating: /var/www/answers/answers_mysql/META-INF/
------ Output removed - comment by Igor ---------------------------------   
  inflating: /var/www/answers/answers_mysql/utils/Utils.class.php  
  inflating: /var/www/answers/answers_mysql/utils/init.php  
--------------
drop database answers
--------------
 
--------------
create database answers
--------------
------ Output removed - comment by Igor --------------------------------- 
--------------
GRANT SELECT ON USERS_VIEW_NAME TO 'ANSWERS_DB_USER'@localhost
--------------
 
SSH: EXEC: completed after 1,003 ms
SSH: Disconnecting configuration [Answers test server] ...
SSH: Transferred 2 file(s)
Build step 'Send files or execute commands over SSH' changed build result to SUCCESS
Finished: SUCCESS
 
1-11 - Jenkins среагировал на коммит и делает check out a fresh copy
12-40  - выполняется build.xml с target build
24-28 - происходит ‘svn upgrade’ рабочей копии
29-33 - файл который декоратор преобразует в простой формат
44-46 - результаты были отправлены на deployment хост и запустился скрипт publish.sh
49-52 - распаковался prj.zip в корень виртуального хоста Apache
53-63 - выполнились SQL скрипты, которые запускает publish.sh

В следующем посте мы расширим созданную нами Jenkins job и запустим Selenium тесты в Nerrvana.

Print this post | Home

2 comments

  1. Emergy says:

    А где возможность отката?

  2. admin says:

    Алекс, вы имеете в виду откат в предыдущее состояние в случае, если в процессе деплоймента произошла ошибка? Дело в том, что в данном посте описывается процесс деплоймента тестовых систем, на которых затем запускаются селениумные тесты. Поэтому нет необходимости возращаться в предыдущее состояние, проще перезапустить процесс после устранения причин ошибки.