Статья будет полезна тем, кто использует TFS как CI-решение, но не в восторге от его шаблонов сборочного процесса.
В ходе развития нашего проекта (который, по-сути, является бинарно поставляемым модулем для, собственно, продукта компании), мы поняли, что хочется иметь CI-процесс чуть сложнее, чем просто компиляция и запуск модульных тестов.
Вот так выглядел «идеальный» процесс в наших глазах.
Регулярная сборка:
- компиляция и прогон модульных тестов;
- подготовка тестовых семплов (семплы являются частью поставки и используются продуктом на последующих этапах тестирования; подготовка заключается в том, чтобы собрать файлы из разных мест, разложить по определенной структуре папок и запаковать);
- прогон тестов производительности; сохранение результатов (чтобы можно было построить графики); генерация предупреждения, если отклонение превышает среднеквадратичное;
- прогон тестов на ложные и истинные срабатывания; сохранение результатов для построения отчетов и/или дальнейших исследований.
- генерация release notes по багам и задачам из TFS;
- публикация release notes на портале;
- автоматическое прописывание номера сборки в багах и задачах;
- выкладывание подготовленного поставочного архива в определенное место, для дальнейшего использования продуктом.
Рабочий процесс в нашей компании построен целиком на TFS-е, включая трекинг багов и задач; контроль версий и непрерывную интеграцию. CI-сборки в TFS-е задаются с помощью шаблона процесса (build process template). Первое, что мы попробовали сделать для реализации нашего «идеала» — это настроить шаблон процесса под себя и… столкнулись с рядом трудностей.
1. Часть сборочной логики меняется по мере развития модуля (в частности, подготовка тестовых семплов), т.е. эту логику желательно держать в системе контроля версий вместе с другими исходниками модуля. Но шаблон процесса в TFS 2012/2013 хранится отдельно и не версионируется (да, он лежит в VCS, но используется всегда только последняя версия). Почему это проблема? У нас как минимум две ветки: разработческая и стабильная (теоретически, могут еще возникать релизные ветки, если потребуется делать хотфиксы) и, по-крайней мере, некоторые шаги сборки в них отличаются, т.е. мы не можем собирать все ветки с помощью одного шаблона.
Возможные решения:
- Версионировать шаблон «вручную», добавляя в название файла шаблона номер версии. В этом случае при слиянии веток нужно не забывать и про слияние шаблонов. А если вдруг кроме правки шаблона понадобятся собственные активности, то псевдо-версионирование придется применять еще и к dll-кам с этим активностями!
- Вынести меняющуюся логику в скрипт и вызвать этот скрипт из шаблона. Скрипт хранить вместе с остальными исходниками модуля.
Мы выбрали скрипт. На первом этапе — обычный bat-файл.
2. Использование скрипта привело нас к другой проблеме — информативность логирования резко упала. Сообщения просто пишутся на консоль, у них нет уровней, нельзя отличить предупреждение от диагностики. Можно выделить только ошибку, записав сообщение в stderr. Но каждая строка, вычитываемая TFS-ом из stderr, будет им восприниматься как отдельная ошибка. Т.о., например, отформатированное 3-х строчное сообщение об ошибке в TFS-е будет светиться как 3 отдельные ошибки. «Не аккуратненько», но жить можно.
3. Скрипт начал расти. Кроме шага подготовки семплов в нем вскоре оказались и тесты производительности, и FP/TP тесты. Почему так? Казалось бы, запуск тестов должен быть в шаблоне. Дело в том, что данные тесты проводятся инструментами, которые также являются частью нашего модуля и, соответственно, развиваются вместе с ним, поэтому и способ запуска тоже меняется. Оставляя их в шаблоне, имеем уже рассмотренную в п.1 проблему.
Дальше — больше. Начало назревать желание иметь доступ из скрипта к свойствам текущей сборки (название ветки, номер версии и т.п.), а также связываться с другими сборками для организации конвейера.
На данном этапе стало понятно, что скрипт должен быть более мощным, чем просто bat-файл. А учитывая, что возиться с шаблонами процесса не ахти как удобно, возникло желание перенести всю логику в скрипт, а шаблон использовать только как оболочку для выкачивания исходников и передачи управления скрипту.
Кандидатами для скрипта стали PSake, Rake и Fake. После беглого анализа PSake отпал из-за PowerShell-синтаксиса; Rake — т.к. с Ruby никто в нашей команде знаком не был; и остался Fake. Справедливости для, надо сказать, что F# на тот момент в команде тоже никто не знал, однако F# поддерживается Visual Studio из коробки, поэтому остановились на нем.
К сожалению, Fake с TFS-ом не дружит. Но что, мы не программисты что ли?! Прикрутили сами. Можно сказать, проблемы 1 и 3 решились. Однако проблема 2 усугубилась, т.к. теперь вся сборка шла в скрипте, результат, показываемый в Visual Studio, выглядел удручающе — предупреждений не видно (в том числе и от компилятора); какие этапы выполнялись — не видно; ошибки показываются жуткими «портянками». Пытались порешать эту проблему, написав расширение для Fake, но в Fake не оказалось единой точки логирования, из которой можно было бы перенаправить структурированный вывод в TFS. Попутно возникло еще несколько неудовлетворенностей (подробнее можно почитать тут).
Тем не менее, идея использовать полноценный язык программирования для описания сборки понравилась. В итоге я решил на досуге воплотить идею Fake-а в собственном исполнении. Упор был сделан на:
- расширяемость (например, подключаемый html-отчет о сборке или публикация результатов тестов в базу данных и т.п.);
- интеграцию с различными внешними системами (первым в очереди был TFS);
- читабельный и понятный лог и вывод ошибок.
Кроме того, вспомнив свои первые попытки понять F#-скрипты, я подумал: «неплохо было бы иметь возможность писать скрипты и на С# тоже» и включил это требование в скоуп работ. Да, можно писать сборочный скрипт на C#, правда, к сожалению, IntelliSense в этом случае не работает — не знает студия, что C# может быть скриптом.
В результате получился весьма приличный инструмент (я назвал его AnFake = Another F# Make), который может быть полезен всем, кто «воюет» с TFS-ом. Давайте посмотрим, как это выглядит и что он может (в данный момент AnFake в основном рассчитан на TFS, поэтому дальнейшее изложение пойдет в контексте TFS-а).
Пусть у нас есть solution под названием Demo, который лежит в системе контроля версий TFS:
$/TeamProject/Demo
/dev
/Demo.App
/Demo.Lib
/Demo.Lib.Test
Demo.sln
Пусть также у нас настроен workspace ‘Demo.dev’ с единственным мапингом:
$/TeamProject/Demo/dev: C:\Projects\Demo.dev
(далее везде предполагается, что используется схема “один workspace на ветку”)
Открываем C:\Projects\Demo.dev\Demo.sln в Visual Studio. Устанавливаем AnFake как NuGet пакет:
PM> Install-Package AnFake
При установке пакета в корневой папке solution-а будет создано несколько файлов:
- build.fsx — базовый сборочный скрипт, включающий вызов MSBuild и запуск тестов;
- anf.cmd — алиас для вызова AnFake (чтобы не писать каждый раз ./packages/AnFake.x.y.z/bin/AnFake.exe);
- .workspace — текстовый файл с описанием мапингов из workspace-а, в рамках которого был скачен текущий solution;
- .nuget\NuGet.config — [создается только, если его не было] содержит опцию disableSourceControlIntegration, чтобы предотвратить комит бинарных файлов пакетов в VCS.
Базовый скрипт build.fsx выглядит следующим образом:
Tfs.PlugIn()
let out = ~~".out"
let productOut = out / "product"
let testsOut = out / "tests"
let tests = !!"*/*.Test.csproj"
let product = !!"*/*.csproj" - tests
"Clean" => (fun _ ->
let obj = !!!"*/obj"
let bin = !!!"*/bin"
Folders.Clean obj
Folders.Clean bin
Folders.Clean out
)
"Compile" => (fun _ ->
MsBuild.BuildRelease(product, productOut)
MsBuild.BuildRelease(tests, testsOut)
)
"Test.Unit" => (fun _ ->
VsTest.Run(testsOut % "*.Test.dll")
)
"Test" <== ["Test.Unit"]
"Build" <== ["Compile"; "Test"]
Tfs.PlugIn();
var outDir = ".out".AsPath();
var productOut = out / "product";
var testsOut = out / "tests";
var tests = "*/*.Test.csproj".AsFileSet();
var product = "*/*.csproj".AsFileSet() - tests;
"Clean".AsTarget().Do(() =>
{
var obj = "*/obj".AsFolderSet();
var bin = "*/bin".AsFolderSet();
Folders.Clean(obj);
Folders.Clean(bin);
Folders.Clean(out);
});
"Compile".AsTarget().Do(() =>
{
MsBuild.BuildRelease(product, productOut);
MsBuild.BuildRelease(tests, testsOut);
});
"Test.Unit".AsTarget().Do(() =>
{
VsTest.Run(testsOut % "*.Test.dll");
});
"Test".AsTarget().DependsOn("Test.Unit");
"Build".AsTarget().DependsOn("Compile", "Test");
В принципе, этого уже достаточно, чтобы запустить локальную сборку:
PM> .\anf Build
(здесь мы использовали Package Manager Console, но AnFake можно запускать из любой консоли командной строки)
В результате получим примерно такой отчет:
Видим, что компиляция прошла с одним предупреждением; было выполнено 2 теста, один из которых Skipped. Ok, комитаем изменения. Комит будет содержать файл .nuget/packages.config (сюда NuGet прописывает пакеты solution-уровня) и файлы, созданные во время установки AnFake-а в корневой папке solution-а.
Теперь запустим эту же сборку через TFS. Для этого нужно установить специальный шаблон AnFakeTemplate.xaml (делается только один раз для team project-а):
PM> .\anf "[AnFakeExtras]/vs-setup.fsx" "BuiltTemplate" -p "TeamProject"
где вместо TeamProject, естественно, нужно подставить имя вашего проекта в TFS-е.
Команда создаст временный workspace с именем AnFake.BuildTemplate.yyyymmdd.hhmmss; выкачает во временную папку $/TeamProject/BuildProcessTemplates; добавит шаблон AnFakeTemplate.xaml и несколько сопутствующих библиотек. Команда ничего НЕ комитает автоматически, дабы не вызвать бурю справедливого возмущения. Поэтому идем в Visual Studio -> Team Explorer -> Pending Changes, переключаемся на workspace AnFake.BuildTemplate, просматриваем изменения (убеждаемся, что там ничего лишнего) и комитаем.
Теперь можем создать определение сборки (build definition):
- Идем в Visual Studio -> Team Explorer -> Builds, выбираем New Build Definition.
- На вкладке Process, в секции Build Process Templates нажимаем Show details.
- Нажимаем New и в поле Version control path вводим (или выбираем через Source Control Explorer) $/TeamProject/BuildProcessTemplates/AnFakeTemplate.v2.xaml; жмем Ok (делается только один раз для team project-а)
- В выпадающем списке Build process file выбираем AnFakeTemplate.v2.xaml и сохраняем.
Шаблон AnFakeTemplate делает три простых шага:
- Выкачивает solution из системы контроля версий.
- Восстанавливает NuGet-пакеты solution-уровня (т.е. скачивает собственно AnFake).
- Передает управление в AnFake.
- Все остальное определяется уже в скрипте.
Шаблон достаточно простой и сохраняет совместимость на протяжении целого ряда версий. Таким образом вы можете апгрейдить пакет AnFake без необходимости обновления шаблона. Можно даже в разных solution-ах иметь разные версии AnFake и они будут благополучно собираться одним шаблоном.
Я продемонстрировал базовый сценарий интеграции AnFake в TFS. Однако интеграция не ограничивается шаблоном: есть возможность из скрипта обращаться к свойствам текущей сборки; получать доступ к артефактам других сборок; есть даже возможность организовать конвейер. Кроме того, AnFake предоставляет дополнительную автоматизацию при работе с мапингами и workspace-ами: мапинги можно хранить в VCS вместе с остальными исходниками проекта, workspace будет создаваться и обновляться автоматически.
Не хочется перегружать статью описанием всех этих плюшек здесь и сейчас, но если данный подход интересен и инструмент востребован, то в ближайшее время напишу продолжение. В заключении хочется сказать, что «идеальная» сборка, описанная в начале статьи, в настоящий момент полностью реализована и работает на AnFake. Я не могу показать наши реальные скрипты, но некоторые интересные выдержки можно посмотреть здесь.
Буду благодарен за обратную связь. Спасибо.
Комментарии (4)
pashuk
26.04.2015 23:19Зря не взяли psake.
Вы ограничились в требованиях прогоном юнит тестов, а на этом мир не заканчивается.
Continuous Integration на F# вы построите, а Continuous Delivery — не построите.
Как только появятся требования вида «скопировать на удалённый комп», «сконфигурировать IIS под web сайт», «развернуть в Azure», «перезапустить SQL Server», то F# станет бесполезен, а полезен станет powershell.
Для SQL Server, IIS, SharePoint есть стандартные powershell модули от microsoft, надо будет их использовать для автоматизации процесса развёртывания.
F# тут всегда будет в догоняющих.
На крупном проекте от powershell всё равно уйти не получится, не проще ли сразу взять psake?IlyaAI Автор
27.04.2015 09:16Во-первых, вообще-то не ограничился «прогоном юнит тестов», у нас довольно много шагов выходящих за рамки юнит тестов. Я упоминаю об этом в описании «идеального» процесса.
Во-вторых, Вы справедливо разделили Continuous Integration и Continuous Delivery. И из того, что PowerShell хорошо подходит для delivery, не следует, что и все другие задачи тоже следует решать с его помощью. В наших задачах PowerShell сильно проигрывал F#.
Кстати, оба подхода вполне могут «жить дружно» — не вижу ничего плохого, в том, что delivery будет описываться отдельным ps-скриптом и выполняться в рамках своего build definition-а в TFS-е. И даже, если очень хочется, чтобы это был один скрипт, то можно поднять PowerShell host внутри AnFake-а и вызвать стандартные PowerShell модули от Microsoft.
vba
Fake не плохой, просто он базируется от части на nant и прочих пережитках эпохи, отсюда и лимиты. TFS 2015 обещает совершенно иной подход к построению приложений, он будет больше ориентирован на облако и будет поддерживать задачи различной природы и типа. Не хочу вас огорчать но все что вы сделали потеряет актуальность, осталось только набраться терпения и подождать.
IlyaAI Автор
Вполне возможно, Вы правы насчет потери актуальности. Но у меня были задачи, которые надо было решить здесь и сейчас. В процессе решения появился инструмент, о коем я и рассказал — может кому-то будет полезен.