Skip to content

Instantly share code, notes, and snippets.

@aminin
Created October 20, 2011 09:50
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aminin/1300783 to your computer and use it in GitHub Desktop.
Save aminin/1300783 to your computer and use it in GitHub Desktop.
Разворачивание проекта с помощью Phing

Использование Phing

Общие замечания

Директория с проектом на продуктовом сервере имеет следующую структуру:

<директория с проектом на сервере>
    current             Ссылка на ./versions/release-002
                        Веб сервер ищет DOCUMENT_ROOT внутри этой директории
    versions            Директория с версиями проекта
        release-001     Первая версия
        release-002     Вторая версия
    workingcopy         Рабочая копия с .git внутри. отсюда можно
                        устанавливать версии, выполнять команды git и phing
    shared              Директория для общих между версиями файлов
        web
            uploads

Прежде чем начать

Убедитесь что в вашей системе утановлен phing, если не установлен - установите отсюда

Сотворите обряд установки всего необходимого для проекта (php, nginx, и т. д.)

Клонируйте хранилище кода проекта.

$ git clone <адрес хранилища> .
$ git submodule update --init

Создайте файл конфигурации сборщика build/properties/config.conf со своими настройками

Установка на машине разработчика

$ cd build
$ phing update -Drev=development

Установка на продуктовом сервере

$ cd build
$ phing install -Drev=<Имя тэга или ветки>
$ phing switch -Drev=<Имя тэга или ветки>

Между установкой и переключением оператор проверяет что установка прошла без ошибок и новая версия готова стать текущей.

Выкатка ветки для демонстрации (допиcать)

$ тут какая-то магия с созданием отдельной БД для этой ветки
$ cd build
$ phing install -Drev=<Имя тэга или ветки> -Dload-data=1

И потом кто-то или что-то должно удалять директории и БД мёртвых веток.

Смена ветки и проблема миграции

Имеем ветки A и B с общим предком P, которым соответствуют ревизии хранилища миграций a, b и p, соответственно. Чтобы перейти в ветку B из A, нужно откатить миграции до состояния p, перейти в ветку B и накатить миграции b.

$ symfony doctrine:migrate <Номер миграции p>
$ cd build
$ phing update -Drev=<Ветка A>

Мне пока не понятно как настроить Phing, чтобы он делал всё вышеописанное одной командой.

Команды и их параметры

  • build создаёт директории, накатывает миграции, кладёт тестовые данные в БД Параметры:
    • no-migrate не накатывать миграции
    • load-data накатить тестовые данные в чистую БД
  • checkout переключает локальное хранилище на заданную ветку Параметры:
    • rev имя ветки или тэга (может быть указано в конфиге project.git.rev)
  • deploy копирует файлы в директорию тэга или ветки Параметры:
    • rev имя ветки или тэга (может быть указано в конфиге project.git.rev)
  • install update + deploy
  • release копирует файлы в архив
  • switch переключает ссылку current на указанный тэг или ветку Параметры:
    • rev имя ветки или тэга (может быть указано в конфиге project.git.rev)
  • update checkout + build
# Скопируй этот файл в config.conf и измени параметры на свои
# Директория, куда выкатываем готовые сборки
# Используется для команды `phing install`
project.deploy.path = /var/www/vhosts/my-project.dev/
# Пользователь в системе для установки прав на директории и файлы
project.deploy.user = anton
project.deploy.group = www-data
# База данных
project.db.host = localhost
project.db.user = aminin
project.db.pass = Pa55w0rd
project.db.base = aminin
# Тестовая база данных
project.testdb.host = localhost
project.testdb.user = tester
project.testdb.pass = tester_pass
project.testdb.base = testdb
# Редис
project.redis.host = 127.0.0.1
project.redis.port = 6379
project.redis.database = 0
project.redis.password =
# Внешний сервер аутентификации
project.saml.idp = http://idp.example.com/saml2/idp/metadata.php
project.payments.url = http://payments.example.com
git.bin.path = git
project.shared.dir = ${project.basedir}
<?xml version="1.0" encoding="UTF-8"?>
<project name="System paths" default="System paths prepare">
<!-- Подготовка системных директорий-->
<target name="System paths prepare">
<echo>Prepare paths...</echo>
<if>
<available file="${project.basedir}/cache" type="dir" />
<then>
<echo>${project.basedir}/cache alteady exists</echo>
</then>
<else>
<mkdir dir="${project.basedir}/cache" />
<chmod file="${project.basedir}/cache" mode="0775" failonerror="true" />
<chown file="${project.basedir}/cache" user="${project.deploy.user}.${project.deploy.group}" failonerror="false" />
</else>
</if>
<if>
<available file="${project.basedir}/log" type="dir" />
<then>
<echo>${project.basedir}/log alteady exists</echo>
</then>
<else>
<mkdir dir="${project.basedir}/log"/>
<chmod file="${project.basedir}/log" mode="0775" failonerror="true" />
<chown file="${project.basedir}/log" user="${project.deploy.user}.${project.deploy.group}" failonerror="false" />
</else>
</if>
<!-- Директория с общими файлами для разных версий -->
<if>
<not>
<available file="${project.shared.dir}" type="dir" />
</not>
<then>
<mkdir dir="${project.shared.dir}"/>
<chown file="${project.shared.dir}" user="${project.deploy.user}.${project.deploy.group}" />
</then>
</if>
<!-- Пока единственная общая директория web/uploads -->
<if>
<not>
<available file="${project.shared.dir}/web/uploads" type="dir" />
</not>
<then>
<mkdir dir="${project.shared.dir}/web/uploads" />
<chown file="${project.shared.dir}/web/uploads" user="${project.deploy.user}.${project.deploy.group}" />
<chmod file="${project.shared.dir}/web/uploads" mode="0775" failonerror="true" />
</then>
</if>
<!-- Если web/uploads хранится где-то не у нас, ставим на неё ссылку -->
<if>
<not>
<available file="${project.basedir}/web/uploads" />
</not>
<then>
<exec command="ln -s ${project.shared.dir}/web/uploads ${project.basedir}/web/uploads"
checkreturn="true" />
</then>
</if>
</target>
<target name="Deploy paths prepare">
<echo>Prepare deploy paths...</echo>
<if>
<available file="${project.deploy.path}/versions" type="dir" />
<then>
<echo>${project.deploy.path}/versions alteady exists</echo>
</then>
<else>
<if>
<available file="${project.deploy.path}" type="dir" />
<then />
<else>
<fail message="Expected deploy directory '${project.deploy.path}' exists and is writable" />
</else>
</if>
<mkdir dir="${project.deploy.path}/versions" />
<chown file="${project.deploy.path}/versions" user="${project.deploy.user}.${project.deploy.group}" failonerror="false" />
</else>
</if>
</target>
</project>
<?php
/**
* Task to copy files given by regexp names.
*
* @author Anton Minin <anton.a.minin@gmail.com>
*/
/**
* Uses the Phing CopyTask
*/
require_once 'phing/tasks/system/CopyTask.php';
/**
* Task to copy files by regexp names.
*/
class RegexpCopy extends CopyTask
{
protected $fromRegexp;
protected $toRegexp;
protected $dir;
public function setFromRegexp($fromRegexp)
{
$this->fromRegexp = $fromRegexp;
}
public function setToRegexp($toRegexp)
{
$this->toRegexp = $toRegexp;
}
public function setDir($dir)
{
$this->dir = $dir;
}
public function main()
{
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->dir));
$totalFilesCount = 0;
foreach ($iterator as $item) {
if (!$item->isFile())
continue;
$totalFilesCount++;
/** @var $item SplFileInfo */
if (preg_match("#" . $this->fromRegexp . "#", $item->getPathname())) {
$srcPathName = $item->getPathname();
$dstPathName = preg_replace("#" . $this->fromRegexp . "#", $this->toRegexp, $srcPathName);
$srcFile = new PhingFile($srcPathName);
$destFile = new PhingFile($dstPathName);
if ($this->overwrite === true || ($srcFile->lastModified() > $destFile->lastModified()))
$this->fileCopyMap[$srcFile->getAbsolutePath()] = $destFile->getAbsolutePath();
else
$this->log(sprintf('%s omitted, is up to date', $srcFile->getName()));
if (!$this->file) {
$this->setFile($srcFile);
$this->setToFile($destFile);
}
}
}
$this->log(
sprintf(
'Copiyng files by filename regexp: %d matches of %d files in directory %s',
count($this->fileCopyMap),
$totalFilesCount,
$this->dir
)
);
if (count($this->fileCopyMap) == 0) {
$this->log('Nothing to copy');
return true;
}
parent::main();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project name="symfony build" default="symfony-build">
<taskdef name="regexp-copy" classname="tasks.RegexpCopy" classpath="${project.basedir}/build/lib"/>
<!-- Загрузка конфигов -->
<echo>Load config ...</echo>
<if>
<available file="${project.basedir}/build/properties/config.conf" />
<then>
<echo>Use file ${project.basedir}/build/properties/config.conf</echo>
<property file="${project.basedir}/build/properties/config.conf" />
<property file="${project.basedir}/build/properties/defaults.conf" override="no" />
</then>
<else>
<fail message="Not found file ${project.basedir}/build/properties/config.conf" />
</else>
</if>
<!-- Проверяем, если у нас есть алиас rev для свойства project.git.rev, то ставим алиас -->
<if>
<isset property="rev" />
<then>
<property name="project.git.rev" value="${rev}" override="true" />
</then>
</if>
<!-- Приводим имена директорий в нижний регистр -->
<php function="strtolower" returnProperty="strlowerpath">
<param value="${project.git.rev}"/>
</php>
<import file="${project.basedir}/build/lib/paths.target.xml" />
<target name="install" depends="update, deploy" />
<target name="update" depends="checkout, build" />
<!--
Сборка проекта
@param no-migrate - не накатывать миграции
@param load-data - накатить тестовые данные в чистую БД
-->
<target name="symfony-build" depends="System paths prepare">
<!-- Получаем текущее время -->
<tstamp>
<format property="build.timestamp" pattern="%Y%m%d%H%I" />
</tstamp>
<echo>Copying config files</echo>
<regexp-copy
fromRegexp="(${project.basedir}.*?)\.autocopy\.([a-z]+)"
toRegexp="$1.$2"
dir="${project.basedir}"
overwrite="true"
verbose="true">
<filterchain>
<expandproperties />
</filterchain>
</regexp-copy>
<exec command="php ${project.basedir}/symfony cc" dir="${project.basedir}" checkreturn="true" />
<if>
<isset property="no-migrate" />
<then>
<echo>Do not migrate</echo>
</then>
<else>
<echo>Migrating Doctrine DB</echo>
<exec command="php ${project.basedir}/symfony doctrine:migrate" dir="${project.basedir}" checkreturn="true" outputProperty="migration.output" />
<echo level="warning" msg="${migration.output}" />
</else>
</if>
<if>
<isset property="load-data" />
<then>
<echo>Loading common data fixtures</echo>
<exec
command="php ${project.basedir}/symfony doctrine:data-load ${project.basedir}/data/fixtures/common/"
dir="${project.basedir}" checkreturn="true" outputProperty="migration.output" />
<echo level="warning" msg="${migration.output}" />
<echo>Loading common data fixtures</echo>
<exec
command="php ${project.basedir}/symfony doctrine:data-load ${project.basedir}/data/fixtures/users/"
dir="${project.basedir}" checkreturn="true" outputProperty="migration.output" />
<echo level="warning" msg="${migration.output}" />
</then>
</if>
</target>
<!--
Обновление репозитория проекта
@param rev - имя ветки или тэга. Может быть указано в конфиге как
project.git.rev
-->
<target name="checkout">
<if>
<!-- Если текущая ветка доступна - обновляем -->
<available file="${project.basedir}/.git" type="dir" />
<then>
<!-- cd BASEDIR; git fetch -->
<echo>git fetch repo ${project.git.rev}</echo>
<exec command="${git.bin.path} fetch" dir="${project.basedir}" passthru="true" checkreturn="true" />
<exec command="${git.bin.path} fetch --tags" dir="${project.basedir}" passthru="true" checkreturn="true" />
<echo>git remote prune origin</echo>
<exec command="${git.bin.path} remote prune origin" dir="${project.basedir}" passthru="true" checkreturn="true" />
<echo>Use git branch ${project.git.rev}</echo>
<!-- cd BASEDIR; git checkout branch -->
<exec command="${git.bin.path} checkout ${project.git.rev}"
dir="${project.basedir}" passthru="true" checkreturn="true" />
<exec command="${git.bin.path} clean -f -f -d"
dir="${project.basedir}" passthru="true" checkreturn="false" />
<exec command="${git.bin.path} submodule update --init --recursive"
dir="${project.basedir}" passthru="true" checkreturn="true" />
</then>
<!-- Если нет - ругаемся. Делай клон вручную -->
<else>
<fail message="Expected git repositary. Clone manually." />
</else>
</if>
</target>
<!-- RELEASE: Пакуем файлы проекта в архив -->
<target name="release">
<!-- При упаковке в архив ссылки остаются ссылками, а не превращаются в
настоящие файлы. Так что при распаковке на другой машине есть вероятность
получить пачку битых ссылок. -->
<exec command="tar --exclude-from ${project.basedir}/build/properties/deploy.exclude -cpf ${project.basedir}/release.tar ." dir="${project.basedir}" checkreturn="true" />
</target>
<!--
Копируем проект в отдельную директорию и перебрасываем симлинк
@param rev - имя ветки или тэга. Может быть указано в конфиге как
project.git.rev
-->
<target name="deploy" depends="Deploy paths prepare">
<!--Директории для выкатки -->
<delete dir="${project.deploy.path}/versions/${project.git.rev}" includeemptydirs="true" verbose="false" failonerror="true" />
<mkdir dir="${project.deploy.path}/versions/${project.git.rev}"/>
<!-- Копируем базовую директорию в дирректорию для сборки -->
<exec command="tar --exclude-from ${project.basedir}/build/properties/deploy.exclude -cpf - . | (cd ${project.deploy.path}/versions/${strlowerpath} &amp;&amp; tar --same-owner -xpBf -)" dir="${project.basedir}" checkreturn="true" />
<if>
<available file="${project.deploy.path}/versions/${project.git.rev}/web/uploads" />
<then />
<else>
<!-- Ставим ссылку на общую для всех версий директорию -->
<exec command="ln -s ${project.basedir}/web/uploads ${project.deploy.path}/versions/${project.git.rev}/web/uploads" checkreturn="true" />
</else>
</if>
</target>
<!--
Переключаем текущую версию
@param rev - имя ветки или тэга. Может быть указано в конфиге как
project.git.rev
-->
<target name="switch">
<if>
<!--Если текущая ветка доступна - обновляем -->
<available file="${project.deploy.path}/versions/${project.git.rev}" type="dir" />
<then>
<!-- Создаём симлинк на текущую дирректорию -->
<echo>Link branch '${strlowerpath}' to current...</echo>
<exec command="rm current" dir="${project.deploy.path}" />
<exec command="ln -s ${project.deploy.path}/versions/${strlowerpath} current"
dir="${project.deploy.path}" checkreturn="true" />
</then>
<else>
<fail message="Expected project directory '${project.deploy.path}/versions/${project.git.rev}'. Make before 'phing install -Drev=${project.git.rev}'" />
</else>
</if>
</target>
</project>
<?php
/**
* Task to copy files given by regexp names.
*
* @author Anton Minin <anton.a.minin@gmail.com>
*/
/**
* Uses the Phing CopyTask
*/
require_once 'phing/tasks/system/CopyTask.php';
/**
* Task to copy files by regexp names.
*/
class RegexpCopy extends CopyTask
{
protected $fromRegexp;
protected $toRegexp;
protected $dir;
public function setFromRegexp($fromRegexp)
{
$this->fromRegexp = $fromRegexp;
}
public function setToRegexp($toRegexp)
{
$this->toRegexp = $toRegexp;
}
public function setDir($dir)
{
$this->dir = $dir;
}
public function main()
{
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->dir));
$totalFilesCount = 0;
foreach ($iterator as $item) {
if (!$item->isFile())
continue;
$totalFilesCount++;
/** @var $item SplFileInfo */
if (preg_match("#" . $this->fromRegexp . "#", $item->getPathname())) {
$srcPathName = $item->getPathname();
$dstPathName = preg_replace("#" . $this->fromRegexp . "#", $this->toRegexp, $srcPathName);
$srcFile = new PhingFile($srcPathName);
$destFile = new PhingFile($dstPathName);
if ($this->overwrite === true || ($srcFile->lastModified() > $destFile->lastModified()))
$this->fileCopyMap[$srcFile->getAbsolutePath()] = $destFile->getAbsolutePath();
else
$this->log(sprintf('%s omitted, is up to date', $srcFile->getName()));
if (!$this->file) {
$this->setFile($srcFile);
$this->setToFile($destFile);
}
}
}
$this->log(
sprintf(
'Copiyng files by filename regexp: %d matches of %d files in directory %s',
count($this->fileCopyMap),
$totalFilesCount,
$this->dir
)
);
if (count($this->fileCopyMap) == 0) {
$this->log('Nothing to copy');
return true;
}
parent::main();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment