Используем сборочные скрипты на F# / C# в TFS 2012

Думаю, не я один вспоминаю Microsoft нехорошими словами, когда приходится модифицировать и/или расширять так называемый шаблон сборочного процесса (build process template) в TFS. Под катом история о том, как мы перешли от Xaml к скриптам на F# / C#. Как мы пытались интегрировать Fake в TFS, но в итоге получилось собственное решение — AnFake.

Статья будет полезна тем, кто использует 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.
(кстати, установочный скрипт — это тоже F#-скрипт для AnFake)

Базовый скрипт 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"]

...то же на C#
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 и сохраняем.
Запускаем сборку по только что созданному определению: Queue Build в контекстном меню. В результате получаем вот такой отчет:


Шаблон AnFakeTemplate делает три простых шага:
  • Выкачивает solution из системы контроля версий.
  • Восстанавливает NuGet-пакеты solution-уровня (т.е. скачивает собственно AnFake).
  • Передает управление в AnFake.
  • Все остальное определяется уже в скрипте.

Шаблон достаточно простой и сохраняет совместимость на протяжении целого ряда версий. Таким образом вы можете апгрейдить пакет AnFake без необходимости обновления шаблона. Можно даже в разных solution-ах иметь разные версии AnFake и они будут благополучно собираться одним шаблоном.

Я продемонстрировал базовый сценарий интеграции AnFake в TFS. Однако интеграция не ограничивается шаблоном: есть возможность из скрипта обращаться к свойствам текущей сборки; получать доступ к артефактам других сборок; есть даже возможность организовать конвейер. Кроме того, AnFake предоставляет дополнительную автоматизацию при работе с мапингами и workspace-ами: мапинги можно хранить в VCS вместе с остальными исходниками проекта, workspace будет создаваться и обновляться автоматически.

Не хочется перегружать статью описанием всех этих плюшек здесь и сейчас, но если данный подход интересен и инструмент востребован, то в ближайшее время напишу продолжение. В заключении хочется сказать, что «идеальная» сборка, описанная в начале статьи, в настоящий момент полностью реализована и работает на AnFake. Я не могу показать наши реальные скрипты, но некоторые интересные выдержки можно посмотреть здесь.

Буду благодарен за обратную связь. Спасибо.
Поделиться публикацией

Комментарии 4

    0
    Fake не плохой, просто он базируется от части на nant и прочих пережитках эпохи, отсюда и лимиты. TFS 2015 обещает совершенно иной подход к построению приложений, он будет больше ориентирован на облако и будет поддерживать задачи различной природы и типа. Не хочу вас огорчать но все что вы сделали потеряет актуальность, осталось только набраться терпения и подождать.
      0
      Вполне возможно, Вы правы насчет потери актуальности. Но у меня были задачи, которые надо было решить здесь и сейчас. В процессе решения появился инструмент, о коем я и рассказал — может кому-то будет полезен.
      0
      Зря не взяли psake.

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

      Continuous Integration на F# вы построите, а Continuous Delivery — не построите.

      Как только появятся требования вида «скопировать на удалённый комп», «сконфигурировать IIS под web сайт», «развернуть в Azure», «перезапустить SQL Server», то F# станет бесполезен, а полезен станет powershell.

      Для SQL Server, IIS, SharePoint есть стандартные powershell модули от microsoft, надо будет их использовать для автоматизации процесса развёртывания.
      F# тут всегда будет в догоняющих.

      На крупном проекте от powershell всё равно уйти не получится, не проще ли сразу взять psake?
        0
        Во-первых, вообще-то не ограничился «прогоном юнит тестов», у нас довольно много шагов выходящих за рамки юнит тестов. Я упоминаю об этом в описании «идеального» процесса.

        Во-вторых, Вы справедливо разделили Continuous Integration и Continuous Delivery. И из того, что PowerShell хорошо подходит для delivery, не следует, что и все другие задачи тоже следует решать с его помощью. В наших задачах PowerShell сильно проигрывал F#.

        Кстати, оба подхода вполне могут «жить дружно» — не вижу ничего плохого, в том, что delivery будет описываться отдельным ps-скриптом и выполняться в рамках своего build definition-а в TFS-е. И даже, если очень хочется, чтобы это был один скрипт, то можно поднять PowerShell host внутри AnFake-а и вызвать стандартные PowerShell модули от Microsoft.

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

      Самое читаемое