Pull to refresh
117.4
Контур
Делаем сервисы для бизнеса

Настройка TeamCity для новичков

Reading time 9 min
Views 158K
Эта статья в первую очередь пригодится тем, кто использует тот же стек технологий, что и наша команда, а именно: ASP.NET, C#, NUnit, Selenium 2, git, MSBuild. Будут рассмотрены такие задачи, как интеграция с git, сборка C#-проектов, NUnit-тесты (как модульные, так и тесты UI), а также деплой на сервер. Впрочем, наверняка найдётся интересное и для других пользователей, кроме разве что съевших на этом вопросе собаку. Но они опять же смогут обратить внимание на ошибки в статье или что-то посоветовать: например, как оптимизировать фазу деплоя.

Что такое «непрерывная интеграция», отлично рассказано здесь и вот здесь, повторять эту тему в сотый раз вряд ли нужно.

Ну и для начала – что может TeamCity (далее – просто TC)? А может он следующее: при появлении изменений в указанной ветке репозитория (или ином событии) выполнить сценарий, включающий в себя, например, сборку приложения, прогон тестов, выполнение других сценариев, заливку файлов на удаленный сервер и т.п.

Важным моментом является то, что «сервер интеграции» и «машина, на которой будет проходить этот процесс», – обычно (не обязательно) разные серверы. Более того, машинок, на которых запускаются сборки и тесты, может быть несколько, даже много, и все на разных ОС – в общем, есть, где развернуться.

Для запуска процесса сборки используется программа-агент, принимающая команды от TC-сервера, запустить ее можно на любой из основных ОС (слава мультиплатформенности Java). На один компьютер можно установить несколько агентов и запускать их параллельно, но важно помнить, что один агент единовременно может обрабатывать только один проект. При старте задачи TC выбирает первый подходящий незанятый агент, причем можно устанавливать «фильтры», например, выбирать агента только с ОС Windows или только с установленным .NET версии не ниже 4.0 и т.п.

Теперь нужно придумать сценарий работы. Мы используем в работе следующие ветки:
  1. release – содержит актуальный код, рабочую версию, которая находится на боевом сервере;
  2. dev – в неё идут все новые фичи, позже вливается в release;
  3. отдельная ветка на каждую фичу, которая отпочковывается от dev и в неё же возвращается.

В общем, практически стандартный git-flow, о котором более подробно можно прочитать, например, здесь.

В связи с этим наш сценарий будет выглядеть так:

  1. забрать свежие изменения из репозитория ветки dev;
  2. скомпилировать проект;
  3. если всё прошло успешно на предыдущем шаге – прогнать юнит-тесты;
  4. если всё прошло успешно на предыдущем шаге – прогнать функциональные тесты;
  5. если всё прошло успешно на предыдущем шаге – залить изменения на тестовый сервер.

Для релизной ветки делаем всё то же самое, но на время заливки новых данных приостанавливаем сервер и показываем заглушку.

А теперь вперёд – реализовывать сценарий!


Забрать свежие изменения из репозитория


Начинается всё с немудрёного создания проекта.



После – создание «build configuration» (конфигурации сборки). Конфигурация определяет сценарий сборки.



На втором же шаге создания конфигурации нас спросят об использованной VCS, так что отвечаем честно, что у нас тут git. У вас может быть другая VCS – не теряйтесь. Добавление нового репозитория производится кнопкой Create and attach new VCS root.



Итак, ключевые настройки:



  • VCS root ID можно не трогать – уникальный код, как ни крути. Если оставить пустым, генерируется автоматически;
  • Fetch URL – тот адрес, с которого мы будем забирать содержимое репозитория;
  • Default branch – ветка, с которой будет браться информация;



  • Authentication method – самое интересное – способ аутентификации. Тут может быть и доступ без авторизации (если данные лежат на внутреннем сервере, например), и по ключу, и по паролю. Для каждого варианта дополнительные поля будут свои, сами понимаете.

Остальное многообразие опций – на ваш вкус и цвет.

Далее, нужно настроить автоматический запуск задания. Идём в «Build Triggers» (условия сборки) и выбираем условие VCS Trigger – при настройках по умолчанию он будет раз в минуту проверять наличие новых коммитов в репозитории, а если таковые найдутся – запустит задание на выполнение.




Скомпилировать проект


Поскольку у нас проект на ASP.NET в виде Solution’а из Visual Studio – тут тоже было всё просто.

Идём в меню «Build Steps» (Шаги сборки), выбираем runner type (а их тут действительно много) и останавливаемся на MSBuild. Почему именно на нём? Он даёт достаточно простой способ описать процесс сборки, даже достаточно сложный, добавляя или удаляя различные шаги в простом XML-файле.





Далее всё элементарно.



Build file path – путь к sln-файлу.
MSBuild version, MSBuild ToolsVersion и Run platform выбираем под требования своего проекта.

Если в проекте несколько конфигураций, то можно использовать опцию Command line parameters для ввода примерно такого ключа:

/p:Configuration=Production

где Production заменить на нужный конфиг.


Включить скачивание NuGet-пакетов


Важный пункт, в случае если вы используете NuGet-пакеты; если нет – можно переходить сразу к следующему пункту.

Поскольку NuGet-пакеты весят немало, да и хранить в репозитории бинарники библиотек без особой необходимости не хочется, можно использовать замечательную опцию NuGet Package Restore:



В этой ситуации бинарники библиотек в репозиторий не включаются, а докачиваются по мере необходимости в процессе сборки.

Но MSBuild – настоящий джентльмен и не будет без разрешения делать лишних телодвижений, поэтому и докачивать пакеты просто так не будет – ему сей процесс нужно разрешить. Для этого придется либо на клиенте установить переменную окружения Enable NuGet Package Restore в значение true, либо пойти в меню Build Parameters и выставить его там.




Прогнать юнит-тесты


У нас юнит-тесты являются отдельным проектом внутри решения. Так что на предыдущем шаге они уже скомпилированы – осталось их запустить.

Добавляем новый шаг, только теперь Runner – это NUnit. Обратите внимание на параметр Execute step: он указывает, при каких условиях нужно выполнять шаг, и имеет 4 значения:
  • If all previous steps finished successfully (zero exit code) – если все предыдущие шаги завершились без ошибок. При этом проверка выполняется чисто на агенте;
  • Only if build status is successful – аналогично предыдущему, но при этом агент ещё и уточняет у сервера TC статус билда. Нужно для более тонкого управления логикой задания, например, если нулевой код возврата конкретного шага для нас является ошибкой;
  • Even if some of the previous steps failed – даже если какой-то из предыдущих шагов завершился с ошибкой;
  • Always, even if build stop command was issued – выполнять шаг, даже если подана команда на отмену выполнения сборки.

Здесь самое важное – указать верный путь к сборке с тестами внутри проекта в графе Run tests from. У нас, например, он выглядит вот так:

%teamcity.build.checkoutDir%\project\project.FuncTests\bin\Dev\project.FuncTests.dll

%teamcity.build.checkoutDir% – это переменная, указывающая на папку, в которую скачиваются данные из репозитория. В принципе, она не обязательна для указывания, т.к. по умолчанию путь идёт именно относительно этой директории, поэтому путь можно было бы сократить до:

project\project.FuncTests\bin\Dev\project.FuncTests.dll




Отдельно отмечу опцию Run recently failed test first – если в предыдущем прогоне какие-то тесты упали, то в следующем запуске первыми запустятся именно они, и вы быстро узнаете об успешности последних изменений.




Прогнать тесты интерфейса (функциональные тесты)


Здесь все гораздо интереснее, чем с юнит-тестами. Кэп тут подсказывает, что, чтобы протестировать проект в браузере, его, т.е. проект, необходимо запустить. Но тут мы схитрили, запуская веб-сервер прямо из кода тестов Selenium:

	[SetUpFixture]
	class ServerInit
	{
		private const string ApplicationName = "justtest";
		private Process _iisProcess;

		private string GetApplicationPath(string applicationName) {
			var tmpDirName=AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\');
			var solutionFolder = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(tmpDirName)));
			string result = Path.Combine(solutionFolder, applicationName);
			return result;
		}

		[SetUp]
		public void RunBeforeAnyTests()
		{
			[…]
			var applicationPath = GetApplicationPath(ApplicationName);
			var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);

			_iisProcess = new Process
			{
				StartInfo =
				{
					FileName = string.Format("{0}/IIS Express/iisexpress.exe", programFiles),
					Arguments = string.Format("/path:\"{0}\" /port:{1}", applicationPath, UrlProvider.Port)
				}
			};
			_iisProcess.Start();
		}

		[TearDown]
		public void RunAfterAnyTests()
		{
			[…]
			if (_iisProcess.HasExited == false)
			{
				_iisProcess.Kill();
			}
		}
	}]


А сам запуск выглядит абсолютно точно так же, как на шаге с юнит-тестами.




Залить изменения на тестовый сервер


Сервер, на котором находится агент TC, и сервер, на котором установлен IIS, – разные серверы, причем, более того, находятся они в разных сетях. Поэтому файлы нужно как-то на конечный сервер доставить. И вот тут решение выбрано, может, не очень элегантное, зато крайне простое. Используем заливку через FTP, причем делает сие за нас MSBuild.

Схема такая:
  1. настраиваем на сервере FTP аккаунт для заливки файлов. Для дополнительной безопасности можно запретить заливку для всех IP, кроме внутреннего, если TC-сервер лежит во внутренней сети, конечно;
  2. устанавливаем на агента MSBuild Community Tasks, чтобы получить возможность использовать задачу «Залить по FTP». Качать тут;
  3. подготовить файл-сценарий для MSBuild, который будет производить следующие действия:
    1. сборка приложения во временной папке;
    2. подмена конфигурационного файла;
    3. заливка файлов по FTP.

Вот так будет выглядеть этот файл (deploy.xml)
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="Build">

	<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v11.0\Web\Microsoft.Web.Publishing.Tasks.dll"/>
	<Import Project="$(MSBuildExtensionsPath32)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

    <PropertyGroup>
        <OutputDir>bin</OutputDir>
		<PublishDir>../output</PublishDir>
		<Configuration>Dev</Configuration>
        <TransformInputFile>..\project\project\Web.template.config</TransformInputFile>
        <TransformFile>..\project\project\Web.$(Configuration).config</TransformFile>
        <TransformOutputFile>..\project\output\Web.config</TransformOutputFile>
        <StackTraceEnabled>False</StackTraceEnabled>
    </PropertyGroup>

    <ItemGroup>
        <ProjectToBuild Include="../project/project.sln">
            <Properties>WebProjectOutputDir=$(PublishDir);OutputPath=$(OutputDir);Configuration=Dev</Properties>
        </ProjectToBuild>
    </ItemGroup>

    <Target Name="Build">
        <MSBuild Projects="@(ProjectToBuild)"/>
    </Target>

	<Target Name="CreateWebConfigs" AfterTargets="Build">
	<TransformXml
		Source="$(TransformInputFile)"
		Transform="$(TransformFile)"
		Destination="$(TransformOutputFile)"
	/>
	</Target>

	<Target Name="AfterBuild" AfterTargets="CreateWebConfigs">
		<PropertyGroup>
			<ftpHost>dev.example.com</ftpHost>
			<ftpUser>ЛОГИН</ftpUser>
			<ftpPass>ПАРОЛЬ</ftpPass>
			<LocalDirectory>..\project\output</LocalDirectory>
		</PropertyGroup>
        <FtpUploadDirectoryContent
			ServerHost="$(ftpHost)"
			Port="21"
			Username="$(ftpUser)"
			Password="$(ftpPass)"
			LocalDirectory="$(LocalDirectory)"
			RemoteDirectory=""
			Recursive="true"
        />
	</Target>
</Project>


А так – задание по его вызову:



Недостаток решения – заливаются ВСЕ файлы, а не только новые изменившиеся. На проекте с кучей файлов верстки или множеством модулей это может стать проблемой из-за затрачиваемого на заливку времени.

Ещё один замеченный недостаток – при встрече файла с нелатиницей в названии падает с ошибкой. Латиница и буквы/цифры обрабатываются нормально. Ноги этой проблемы, похоже, растут из специфики протокола FTP: тот основан на ASCII, но, как именно кодировать не ASCII-символы, он не описывает, предлагая «делать так, как будет понимать ваш сервер». Соответственно, чтобы вылечить проблему, не меняя схему, нужно пропатчить MSBuild Community Tasks, благо, исходники открыты. Ну, или воспользоваться альтернативным способом заливки файлов, например через WinSCP.


Остановка и запуск сервера для релизного сценария


Мы её решаем чуть диким, но симпатичным способом. У IIS’а есть особенность: если в корень сайта положить файл с именем app_offline.html, то сайт отрубается, при обращении ко всем файлам будет выдаваться содержимое этого файла.

Минус – обращение именно что КО ВСЕМ файлам, включая статичные. Так что, если хочется сделать заглушку с оформлением, CSS и картинками, используйте инлайн-стили и data:url, ну, или как вариант – выложите их на отдельном сервере.

Включаем-отключаем мы сервер через WinSCP-сценарий и такие вот файлики:

server_off.cmd
winscp.exe /console /script=server_off.txt

server_on.cmd
winscp.exe /console /script=server_on.txt

server_off.txt
option batch abort
option confirm off

open ftp://ЛОГИН:ПАРОЛЬ@dev.example.com

mv _app_offline.htm app_offline.htm

close

exit

server_on.txt
option batch abort
option confirm off

open ftp://ЛОГИН:ПАРОЛЬ@dev.example.com

rm app_offline.htm

close

exit

Т.е., изначально файл лежит в корне и называется _app_offline.html. Когда нужно заблокировать доступ на время апдейта, мы переименовываем его в app_offline.html. При заливке файлов заливается новый файл _app_offline.html, а после окончания – удаляется файл app_offline.html. И получаем именно то, что было изначально.

В тексте страницы-заглушки крайне рекомендую воспользоваться мета-тегом refresh, который периодически будет обновлять страницу. Если к этому времени процесс обновления завершился, пользователь вернётся обратно в сервис, чему наверняка будет несказанно рад.

Вызов сценария включения заглушки (отключение заглушки происходит аналогично):



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

Данные настройки используются нами на протяжении где-то полугода, и пока ни единой проблемы не возникло – всё работает как часы, что не может не радовать.

На этом всё. С удовольствием выслушаю комментарии и советы по улучшению этих шагов, а также ваши рассказы о том, какие возможности TC вы используете у себя.

Update от 3 ноября 2014 года.
Выбирая Runner Type «Command line» не требуется как-либо экранировать пробелы — Team City озаботится об этом самостоятельно.
Tags:
Hubs:
+31
Comments 12
Comments Comments 12

Articles

Information

Website
tech.kontur.ru
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия