Статьи

Развертывание и выпуск приложений с помощью Phing

Предположим, у вас есть веб-приложение, которое установлено на многих хостах. Каждая установка может иметь собственную конфигурацию, и приложение все еще находится в активной разработке. Вам нужен простой способ развертывания новых функций и исправления ошибок на всех хостах.

В своей предыдущей статье « Использование Phing», инструмента сборки PHP , Shameer дал вам базовое понимание Phing. Вы узнали, как читать и писать файл сборки и каковы его основные компоненты: project , targets , tasks и properties .

В этой статье я покажу вам, как использовать эти знания для написания стандартного файла сборки, который можно настраивать и повторно использовать в реальных приложениях. Мы будем использовать задачи Phing Subversion для управления хранилищем и расширение FileSync для синхронизации вашей локальной установки с удаленным сервером.

Подготовка окружающей среды

Чтобы использовать задачи, связанные с Subversion, вам нужны два пакета PEAR VersionControl_SVN и NET_FTP . Оба могут быть легко установлены с помощью следующих команд:

  $ sudo pear установить VersionControl_SVN
 $ sudo pear установить NET_FTP 

Расширение FileSyncTask является дружественной оболочкой для команды rsync . Он поддерживается Федерико Каргнелутти и может быть загружен с его сайта . После загрузки скопируйте файл FileSyncTask.php в каталог расширений Phing (путь по умолчанию в unix-подобных системах — /opt/phing/tasks/ext , но ваша конфигурация может отличаться).

Я создал пустой репозиторий Subversion под названием «helloworld» и дал ему базовую структуру каталогов с каталогами trunk , branches и tags . Я проверил транк локально и импортировал файлы моего приложения, используя следующую структуру каталогов:

Как видите, есть каталог с именем build, который будет содержать файлы, связанные с Phing.

Основной файл build.xml выглядит так:

 <?xml version="1.0" encoding="UTF-8"?> <project name="HelloWorld" default="hello" basedir="../"> <!-- Load project settings from external file --> <property file="build/config/project.properties" /> <!-- Default empty target --> <target name="hello" description="Displays basic project information"> <echo message="Hello, welcome to ${phing.project.name}!" /> <echo message="Current environment is: ${project.env}" /> </target> </project> 

Существует цель по умолчанию «hello», которая отображает основные данные, такие как имя проекта и текущая среда (производство, разработка, подготовка и т. Д.).

Вместо того чтобы объявлять список свойств внутри файла сборки, Phing велит загрузить настройки проекта из внешнего файла. Путь этого файла указывается относительно атрибута basedir определенного в теге property . Делая это, мы можем повторно использовать один и тот же файл для нескольких проектов.

Файл project.properties — это просто текстовый файл, в котором хранятся настройки с использованием синтаксиса ключ-значение, например:

  ftp.host = host.example.com
 ftp.port = 21
 ftp.username = пример
 ftp.password = 123456
 app.home = $ {ftp.host} / MyApp 

Вы можете использовать тот же синтаксис ‘$ {varname}’ для ссылки на значение ранее объявленных свойств.

Файлы свойств хранятся в каталоге build/config . Также в этом каталоге находится подкаталог hosts , который содержит конкретные настройки клиент-хост, и подкаталог scripts котором хранится шаблон сценария установки. Файлы, оканчивающиеся на «-sample.properties», имеют версии в репозитории и используются в качестве шаблонов, в то время как файлы проекта и хоста помечаются как игнорируемые. Каталог build/export будет содержать файлы, сгенерированные нашими целями.

Чтобы запустить любую из целей, мы должны вызвать Phing из каталога build . Общий синтаксис:

  $ phing   [-D PropertyName = PropertyValue ] 

Опция -D позволяет вам переопределить значение свойства, объявленного с его атрибутом переопределения, установленным в «true».

Цель развертывания

Цель deploy — наш инструмент непрерывной интеграции. Он синхронизирует и развертывает текущую рабочую копию с удаленного хоста с использованием определенной конфигурации (например, servername.properties ), хранящейся в build/config/hosts . Цель должна иметь возможность найти файл .properties для требуемого хоста, а затем подключиться к удаленному хосту с помощью этих настроек и выполнить задачу sync .

Цель запускается с помощью следующей команды:

  $ phing deploy -Dhostname = myhostname 

Код цели:

 <target name="deploy" description="Deploys the current working copy to a remote host using FileSync"> <!-- Default hostname is empty, must be passed from the command line --> <property name="hostname" value="false" override="true" /> <!-- Set default LISTONLY to false --> <property name="listonly" value="false" override="true" /> <property name="hostfile" value="build/config/hosts/${hostname}.properties" /> <!-- Check for specific host/env file, if not fail! --> <available file="${hostfile}" property="hostfilefound" value="true"/> <fail unless="hostfilefound" message="Missing host configuration file (${hostfile})!" /> <!-- Host file exists so loading... --> <property file="${hostfile}" /> <!-- Get timestamp --> <tstamp /> <!-- Set default VERBOSE flag to TRUE --> <if> <not> <isset property="sync.verbose" /> </not> <then> <property name="sync.verbose" value="true" override="true" /> <echo message="The value of sync.verbose has been set to true" /> </then> </if> <!-- Set default DELETE flag to FALSE --> <if> <not> <isset property="sync.delete" /> </not> <then> <property name="sync.delete" value="false" override="true" /> <echo message="The value of sync.delete has been set to false" /> </then> </if> <!-- Get auth info, password will be always required --> <property name="sync.remote.auth" value="${sync.remote.user}@${sync.remote.host}" /> <!-- Perform Sync --> <!-- See: http://fedecarg.com/wiki/filesynctask --> <taskdef name="sync" classname="phing.tasks.ext.FileSyncTask" /> <sync sourcedir="${sync.source.projectdir}" destinationdir="${sync.remote.auth}:${sync.destination.projectdir}" listonly="${listonly}" excludefile="${sync.exclude.file}" delete="${sync.delete}" verbose="${sync.verbose}" /> </target> 

Сначала свойство hostname определяется как перезаписываемое и ему присваивается произвольное значение по умолчанию (я выбрал «true»). Тогда свойство listonly определяется со значением по умолчанию. Это свойство используется задачей sync , и если оно установлено в значение true, то задача будет отображать только список файлов, которые должны быть обработаны, но не будет выполнять фактическую синхронизацию.

Следующие три оператора определяют путь к файлу настроек хоста и используют available задачу, чтобы установить свойство hostfilefound если файл присутствует. Если это не так, свойство не будет установлено, и задача fail прервет сценарий с предоставленным сообщением об ошибке. Если файл свойств хоста присутствует, он загружается; этот файл содержит набор настроек для задачи, сгруппированных с использованием префикса «синхронизация». Все они говорят сами за себя, но стоит обратить особое внимание на sync.delete : если установлено значение true, все удаленные файлы, которых нет в локальной копии, будут удалены. Я рекомендую всегда устанавливать его в ложь, если у вас нет веской причины, чтобы это было правдой

Далее мы можем увидеть два других примера того, насколько мощен Phing: мы используем синтаксис if / then для определения значения свойств sync.delete и sync.verbose .

Последняя часть является актуальной задачей deploy . Оператор taskdef сообщает Phing, что sync является пользовательской задачей, и предоставляет путь к файлу PHP для загрузки, а параметры для этой задачи загружаются из файла хоста. excludefile указывает на текстовый файл, содержащий список шаблонов для исключения из синхронизации, по одному шаблону на строку, используя синтаксис rsync .

Примечание: если ваш удаленный сервер использует файл идентификации SSH, вы должны установить атрибут identityfile с полным путем к вашему файлу ключа.

Цель «подготовить»

Задача prepare цель состоит в том, чтобы создать снимок текущего транка и пометить его для выпуска. Текущий транк копируется в каталог tags/ nameoftag репозитория (например, tags/1.0.1 ).

Код для prepare выглядит так:

 <target name="prepare" description="Prepares a tag in the remote repository"> <!-- Ask for a tag label to copy the current trunk --> <property name="tagLabel" value="false" override="true" /> <!-- The tag name cannot be empty! --> <if> <isfalse value="${tagLabel}"/> <then> <fail message="Invalid tag name!" /> </then> </if> <echo>Preparing tag ${tagLabel}...</echo> <!-- Copy trunk to the new tag under tags/tagLabel --> <svncopy force="true" nocache="true" repositoryurl="${svn.repository.url}/trunk" todir="${svn.repository.url}/tags/${tagLabel}" username="${svn.repository.username}" password="${svn.repository.password}" message="Tag release ${tagLabel}" /> <!-- Switch the working copy repo to the newly created tag --> <svnswitch repositoryurl="${svn.repository.url}/tags/${tagLabel}" username="${svn.repository.username}" password="${svn.repository.password}" todir="." /> <!-- Here you can perform any kind of editing: generate documentation, export SQL files, ecc --> <touch file="README.txt" /> <!-- Commit changes --> <svncommit workingcopy="." message="Finish editing tag ${tagLabel}" /> <echo message="Committed revision: ${svn.committedrevision}"/> <!-- Reset working copy repo to trunk --> <svnswitch repositoryurl="${svn.repository.url}/trunk" /> <echo msg="Tag ${tagLabel} completed!" /> </target> 

В первых двух строках мы устанавливаем значение по умолчанию для метки тега, потому что мы хотим, чтобы оно передавалось из командной строки, затем мы isFalse , чтобы это значение не было пустым, используя оператор if / isFalse .

Затем мы используем задачу svncopy чтобы скопировать наш транк в нужный каталог тегов, и задачу svnswitch чтобы сообщить Subversion, что мы сейчас работаем над вновь созданным тегом. С этого момента мы можем вносить любые изменения в наши файлы, такие как обновление файла README или редактирование файлов конфигурации с подходящими значениями по умолчанию. После того, как все изменения выполнены, мы используем svncommit для сохранения изменений и другой svnswitch для сброса нашей рабочей копии обратно в предыдущее состояние.

Цель выпуска

Цель release — наш упаковочный инструмент. Он начинается с извлечения транка или выбранного тега из хранилища и выполняет с ним следующие действия:

  • экспортировать файлы тегов куда-нибудь (по умолчанию для сборки / экспорта)
  • создать пакет TAR.GZ из экспортированных файлов
  • вычислить дайджест SHA1 сжатого файла
  • создать сценарий установки с помощью файла install.template.sh
  • загрузить файлы на сервер релизов

Код для release :

 <target name="release" description="Exports the trunk or the given tag along with install scripts and FTP uploads"> <property name="release" value="trunk" override="true" /> <echo message="Creating package for '${release}'" /> <!-- Process repository path for trunk or tag --> <if> <equals arg1="${release}" arg2="trunk" /> <then> <property name="repo-path" value="${release}" override="true" /> </then> <else> <property name="repo-path" value="tags/${release}" override="true" /> </else> </if> <!-- Export selected branch/tag from remote repository --> <svnexport repositoryurl="${svn.repository.url}/${repo-path}" force="true" username="${svn.repository.username}" password="${svn.repository.password}" nocache="true" todir="${svn.export.basedir}/${release}" /> <!-- Do other custom editing here... --> <!-- Create TAR archive --> <tar destfile="${svn.export.basedir}/${phing.project.name}-${release}.tar.gz" compression="gzip"> <fileset dir="${svn.export.basedir}/${release}"> <include name="*" /> </fileset> </tar> <!-- Delete Temporary Export directory --> <delete dir="${svn.export.basedir}/${release}" includeemptydirs="true" verbose="false" failonerror="true" /> <!-- Compute SHA1 digest --> <property name="hash" value="empty" /> <filehash file="${svn.export.basedir}/${phing.project.name}-${release}.tar.gz" hashtype="1" propertyname="hash" /> <echo msg="SHA1 Digest = ${hash}" /> <echo msg="Files copied and compressed in build directory OK!" /> <!-- Prepare install.sh, backup.sh and update.sh scripts --> <copy todir="${svn.export.basedir}" overwrite="true"> <mapper type="glob" from="*.template.sh" to="*.sh"/> <fileset dir="./build/config/scripts"> <include name="*.sh" /> </fileset> <filterchain> <replacetokens begintoken="##" endtoken="##"> <token key="SRCURL" value="${http.srcurl}/${release}/" /> <token key="FILENAME" value="${phing.project.name}-${release}" /> <token key="FILEXT" value="tar.gz" /> <token key="HASH" value="${hash}" /> <token key="APPNAME" value="${phing.project.name}" /> <token key="APPVERSION" value="${release}" /> </replacetokens> </filterchain> </copy> <!-- Upload the generated file(s) to FTP --> <property name="upload" value="false" override="true" /> <if> <equals arg1="${upload}" arg2="true" /> <then> <echo msg="Uploading to FTP server for release..." /> <ftpdeploy host="${ftp.host}" port="${ftp.port}" username="${ftp.username}" password="${ftp.password}" dir="${ftp.dir}/${release}" passive="${ftp.passive}" mode="${ftp.mode}"> <fileset dir="${svn.export.basedir}"> <include name="${phing.project.name}-${release}.tar.gz" /> <include name="install.sh" /> </fileset> </ftpdeploy> <echo>Now you can run: wget ${http.srcurl}/${release}/install.sh &amp;&amp; sh install.sh [stage|local|prod] 2>&amp;1 > ./install.log</echo> </then> </if> <echo msg="Done!" /> </target> 

Первые несколько строк касаются входных параметров, с помощью которых мы указываем значение по умолчанию «trunk» для выпуска для экспорта и используем оператор if для вычисления пути к исходному репозиторию.

Задача svnexport экспортирует данные файлы непосредственно из хранилища (не рабочей копии) в наш целевой каталог. Создается временный каталог с именем «AppName-ReleaseName».

Задача tar генерирует файл пакета из временного каталога экспорта, используя внутренний fileset качестве источника. Задача delete , которая является необязательной, удаляет временные файлы.

Затем мы используем задачу filehash для генерации дайджеста SHA1 для упакованного файла (другой вариант — MD5) и сохраняем его в свойстве hash .

Задача copy вызывается для копирования сценария установщика шаблона в наш каталог экспорта и использует некоторые очень мощные ресурсы Phing. mapper — это инструмент выбора и преобразования фильтров, применяемый к filenames . Файлы, выбранные с заданным fileset , обрабатываются картографом перед копированием. В этом случае файлы, соответствующие «* .template.sh», переименовываются с расширением «.sh» в целевом каталоге.

Другая мощная функция, используемая здесь, это filterchain Фильтры Phing используются для преобразования содержимого файлов во время выполнения другой задачи. В этом случае «родительской» задачей является copy . Используемый здесь фильтр — replacetokens , который заменяет в каждом файле набор определенных переменных шаблона динамически генерируемым значением. Он используется здесь для вставки конкретных сведений о выпуске (имя, версия, хеш, URL и т. Д.) В скрипт установки.

Последний штрих — задача ftpdeploy , запускаемая свойством upload . Эта задача загружает файл нашего пакета и скрипт установки на удаленный сервер и отображает URL-адрес, который будет использоваться для вашей установки. Если ошибок нет, вы можете выполнить команду:

  $ wget http://www.yoursite.com/helloworld/releases/trunk/install.sh && sh install.sh 

Команды загружают и проверяют файл пакета, а затем выполняют заданные вами задачи установки.

Резюме

Мы видели много полезных функций Phing здесь, но есть много других, чтобы обнаружить. Вы можете использовать файл сборки «как есть» для развертывания текущих и будущих проектов или расширить его, добавив дополнительные функции, такие как поддержка модульного тестирования, Git, обработка базы данных и даже пользовательские расширения. Как всегда, официальная документация будет вашим лучшим началом.

Изображение через 1971yes / Shutterstock