Перейти к содержимому

Автоматическая модификация umbracoSettings.config

Ещё один кусочек автоматизации, предназначенный упростить жизнь билд инжинеру и убрать необходимость ручного конфигурирования параметров , уже заданных во время конфигурации билд процесса.

Umbraco – это open-source ASP.NET CMS с большим сообществом разработчиков. Одной из особенностей этой системы, которая довольно сильно усложняет автоматическую ее доставку, является система конфигурационных файлов. Конфигурация Umbraco состоит из web.config и целой кучи файлов в папке ~/config. Идея хороша – каждая часть управляется своим конфигурационным файлом и работает всё это отлично… Ровно до тех пор, пока в какой-либо среде не возникает необходимость задать уникальные параметры в этот конфиг файл. Штатные средства Visual Studio (даже VS 2015) не позволяют проводить трансформацию файлов, отличных от web.config в корне приложения. Конечно, есть плагин SlowCheetah, который убирает этот недочёт, но с использованием его возникает другая проблема: зачастую, приходится дублировать билд конфигурации, копировать из предыдущих проектов, что ведет к недочетам при развертывании. Вот для этого я и разрабатываю новые фишки автоматизации – снизить нагрузку от человеческого фактора при конфигурации проекта и вынести в автоматические процедуры то, что возможно.

Этот пост будет посвящен модификации двух частей umbracoSettings.config файла, отвечающих за поддержку KeepAlive в Umbraco, выполнение задач по расписанию и отложенную публикацию контента.
К моему большому сожалению, таких параметров два, и от версии Umbraco зависит, кто из них должен быть добавлен. В версиях 6.2.5 и 7.1.9-7.2.7 – это аттрибут baseUrl элемента scheduledTasks, в версиях 7.2.7+ – это аттрибут umbracoApplicationUrl элемента web.Routing (open-source дарит нам свои веселые плюшки). Суть этих элементов проста – не надеяться на код, который определит, как можно достучаться до CMS из первого запроса, а задать параметром (подробнее, можно почитать тут - https://our.umbraco.org/documentation/reference/config/umbracosettings/ и тут - http://issues.umbraco.org/issue/U4-6788).

Итак, из того что имеется – это проект, настроенный билд в (в котором уже указано, как мы можем попасть на данный сервер в параметре system.website.hostname) и желание автоматизировать внесение этих параметров в конфигурационный файл umbracoSettings.config.

Код и пояснения

В первую очередь надо модифицировать процесс сборки проекта таким образом, чтобы наша задачка выполнялась после компиляции (таким образом, мы её выполним до трансформаций файлов конфигурации) путем включения в umbraco.targets:

    <PropertyGroup>
        <TargetsTriggeredByCompilation>
            $(TargetsTriggeredByCompilation);
            UmbracoSettingsConfigTransform; 
        </TargetsTriggeredByCompilation>    
    </PropertyGroup>

Таким образом, наша цель будет вызвана сразу после компиляции проекта. Далее, в том же PropertyGroup необходимо добавить переменные, которые позволят отменить внесение изменений в конфигурационный файл:

    <PropertyGroup>
             <!-- If system.DoNotSetScheduledTasksBaseUrl is not defined - it shall be equal to false -->
            <DoNotSetScheduledTasksBaseUrl Condition="'$(DoNotSetScheduledTasksBaseUrl)'==''">False</DoNotSetScheduledTasksBaseUrl>
        <!-- 
            As seen by name starting with underscores - __SetScheduledTasksBaseUrl - is internal variable.
            It's target - to establish setup of scheduled task base url
        --> 
        <__SetScheduledTasksBaseUrl>!$(DoNotSetScheduledTasksBaseUrl)</__SetScheduledTasksBaseUrl>
        <!-- If system.DoNotSetUmbracoApplicationUrl is not defined - it shall be equal to false -->
        <DoNotSetUmbracoApplicationUrl Condition="'$(DoNotSetUmbracoApplicationUrl)'==''">False</DoNotSetUmbracoApplicationUrl>
        <__SetUmbracoApplicationUrl>!$(DoNotSetUmbracoApplicationUrl)</__SetUmbracoApplicationUrl>
    </PropertyGroup>

Как видно из примера, если переменные не заданы, то мы их сами ставим в False, разрешая модификации.

Далее, перед началом модификации файла, он копируется во временную директорию и, по завершению билда, достаётся оттуда (во избежание конфликтов при последующих чекаутах проекта) в этой цели:


  <Target Name="CopySourceTransformFiles" BeforeTargets="Cleanup" >
    <Copy SourceFiles="$(UmbracoSettingsConfigFullPath)" DestinationFolder="$(IntermediateOutputPath)" OverwriteReadOnlyFiles="True" />
  </Target>
<Target Name="RestoreSourceTransformFiles" BeforeTargets="Cleanup" > 
    <Copy SourceFiles="$(IntermediateOutputPath)$(UmbracoSettingsConfigName)" DestinationFolder="$(umbracoConfigFolder)" Condition="$(__SetScheduledTasksBaseUrl) Or $(__SetUmbracoApplicationUrl)" />
</Target>

Однако, не всё так просто, и нам необходимо ещё проверить, возможна ли такая модификация в текущей версии Umbraco. Так как Umbraco хранит текущую установленную версию в web.config в appSettings в ключе umbracoConfigurationStatus  - я не вижу смысла вводить дополнительную переменную в билд конфиг на уровне Teamcitу. Мы просто достанем это значение оттуда и при помощи нугет-пакета SemanticVersioning (https://www.nuget.org/packages/SemanticVersioning/, https://github.com/adamreeve/semver.net) проверим, попадает ли версия в нужные промежутки. Данная задачка решается при помощи следующего кода и дополнительно разработанного Task:

<UsingTask TaskName="CompareSemanticVersions" AssemblyFile="$(TeamCityCiToolsPath)\CI.Builds\MsBuildCustomTasks\Colours.Ci.MSBuild.CustomTasks.dll"/>
<Target Name="EstablishConfigFileModificationsRequirements">
    <!-- Get version from web.config -->
    <XmlPeek Namespaces="&lt;Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/&gt;"
      XmlInputPath="web.config"
      Query="configuration/appSettings/add[@key = 'umbracoConfigurationStatus']/@value"
      Condition="Exists('web.config')">
        <Output TaskParameter="Result" PropertyName="__umbracoVersionFromWebConfig" />
    </XmlPeek>
    <Message Text="Umbraco version peeked from web.config: $(__umbracoVersionFromWebConfig)" Importance="high" />
    <!-- 
        Now we have to check, which Umbraco version we are working with.
        baseUrl for scheduled tasks are allowed in 6.2.5 and 7.1.9 - 7.2.7
        If version does not fit in this range - baseUrl shall not be set.
        If version is 7.2.7 and higher - web.routing attribute umbracoApplicationUrl shall be used
    -->
    <CompareSemanticVersions
      CurrentVersion="$(__umbracoVersionFromWebConfig)" 
      AllowedVersionRange="6.2.5 || &gt;=7.1.9 &lt;7.2.7"
      Condition="$(__SetScheduledTasksBaseUrl)">
      <Output TaskParameter="VersionIsInRange" PropertyName="__SetScheduledTasksBaseUrl"/>
    </CompareSemanticVersions>
    <Message Text="We are going to set scheduledTasks baseUrl" Importance="high" Condition="$(__SetScheduledTasksBaseUrl)"/>
    <Message Text="We are NOT going to set scheduledTasks baseUrl" Importance="high" Condition="!$(__SetScheduledTasksBaseUrl)"/>
    <MSBuild 
        Projects="$(MSBuildProjectFile)" 
        Properties="CustomConfigFileToTransfom=$(UmbracoSettingsConfigFullPath);CustomConfigTransformFile=$(TeamCityCiToolsPath)\CI.Builds\SharedFiles\Umbraco\umbraco.umbracosettings.scheduledurl.config.transform" 
        Targets="TransformCustomConfigFile"
        Condition="$(__SetScheduledTasksBaseUrl)" />
    <CompareSemanticVersions
      CurrentVersion="$(__umbracoVersionFromWebConfig)" 
      AllowedVersionRange="&gt;=7.2.7"
      Condition="$(__SetUmbracoApplicationUrl)">
      <Output TaskParameter="VersionIsInRange" PropertyName="__SetUmbracoApplicationUrl"/>
    </CompareSemanticVersions>
    <Message Text="We are going to set web.routing umbracoApplicationUrl" Importance="high" Condition="$(__SetUmbracoApplicationUrl)"/>
    <Message Text="We are NOT going to set web.routing umbracoApplicationUrl" Importance="high" Condition="!$(__SetUmbracoApplicationUrl)"/>
    <MSBuild 
        Projects="$(MSBuildProjectFile)" 
        Properties="CustomConfigFileToTransfom=$(UmbracoSettingsConfigFullPath);CustomConfigTransformFile=$(TeamCityCiToolsPath)\CI.Builds\SharedFiles\Umbraco\umbraco.umbracosettings.umbracoApplicationUrl.config.transform" 
        Targets="TransformCustomConfigFile"
        Condition="$(__SetUmbracoApplicationUrl)" />                    
</Target>

При помощи XmlPeek задачи мы достаём из web.config установленную версию Umbracо. Если этого ключа в web.config нет, то Umbraco будет запускать установку, и, таким образом, мы можем быть уверены, что он там будет.

    <CompareSemanticVersions
      CurrentVersion="$(__umbracoVersionFromWebConfig)" 
      AllowedVersionRange="6.2.5 || &gt;=7.1.9 &lt;7.2.7"
      Condition="$(__SetScheduledTasksBaseUrl)">
      <Output TaskParameter="VersionIsInRange" PropertyName="__SetScheduledTasksBaseUrl"/>
    </CompareSemanticVersions>

CompareSemanticVersions – это и есть дополнительно разработанный MsBuild Task. Он принимает текущую версию в параметре CurrentVersion, диапазон разрешенных версий в нотации NPM (https://github.com/npm/node-semver#ranges) и выдает булевую переменную – возможна ли модификация. Так как используется та же временная переменная, которая объявлена в старте билда – тут мы её можем модифицировать, если версия не попадает в нужный диапазон. Та же самая задачка вызывается для UmbracoApplicationUrl.
Тут есть одна фишка MsBuild – знаки ‘>’ и ‘<’ должны быть представлены как ‘&gt;’ и ‘&lt;’ (https://msdn.microsoft.com/en-us/library/7szfhaft.aspx) – иначе билд будет ломаться.

    <MSBuild 
        Projects="$(MSBuildProjectFile)" 
        Properties="CustomConfigFileToTransfom=$(UmbracoSettingsConfigFullPath);CustomConfigTransformFile=$(TeamCityCiToolsPath)\CI.Builds\SharedFiles\Umbraco\umbraco.umbracosettings.umbracoApplicationUrl.config.transform" 
        Targets="TransformCustomConfigFile"
        Condition="$(__SetUmbracoApplicationUrl)" />

Это – вызов дополнительной цели для подготовки umbracoSettings.config перед собственно модификацией, так как для модификации используется встроенная функция MsBuild XmlPoke, которая (сюрприз!!!) не умеет вставлять аттрибуты. Потому файл umbracoSettings.config модифицируется дополнительным трансформом при помощи указанного ниже кода, который вставляет нужный нам аттрибут, в нужный элемент (в зависимости от версии Umbraco, при условии, что это явно не запрещено):

<Target Name="TransformCustomConfigFile" Condition="Exists('$(CustomConfigFileToTransfom)') And Exists('$(CustomConfigTransformFile)')">
    <RandomNameTask>
      <Output TaskParameter="RandomName" PropertyName="RandomName" />
    </RandomNameTask>
    <!-- Somebody have to ensure, that he have original file copied - but this shall not be done here, IMHO -->
    <!-- let us do a transformation -->
    <MakeDir Directories="$(IntermediateOutputPath)$(RandomName)\"/>
    <TransformXml 
        Source="$(CustomConfigFileToTransfom)" 
        Destination="$(IntermediateOutputPath)$(RandomName)\transformed.$(RandomName).config" 
        Transform="$(CustomConfigTransformFile)" 
        StackTrace="True" />
    <!-- now, result of transformation shall be copied to original place -->
    <Copy 
        SourceFiles="$(IntermediateOutputPath)$(RandomName)\transformed.$(RandomName).config" 
        DestinationFiles="$(CustomConfigFileToTransfom)" 
        OverwriteReadOnlyFiles="True" />
    <!-- And intermediate results shall be deleted -->
    <Delete Files="$(IntermediateOutputPath)$(RandomName)\transformed.$(RandomName).config" />                
</Target>

Финальная задачка – из system.website.hostname и umbracoPath из web.config приложения (параметр, описывающий, как можно попасть в Umbraco) при помощи дополнительных задач MsBuild собираются урлики для доступа к Umbraco. Данное решение пришлось применить потому, что baseUrl не должен содержать схемы (то есть, он должен выглядеть наподобие mysite.com:80/umbraco), а umbracoApplicationUrl должен быть просто FQDN (http://mysite.com/umbraco). Выполняется она при помощи двух последовательный XmlPoke, проверяющих следующие условия:

<XmlPoke Namespaces="&lt;Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/&gt;"
    XmlInputPath="$(UmbracoSettingsConfigFullPath)"
    Query="//settings/scheduledTasks/@baseUrl"
    Value="$(BaseUrl)" 
    Condition="$(__SetScheduledTasksBaseUrl)" />
<XmlPoke Namespaces="&lt;Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/&gt;"
    XmlInputPath="$(UmbracoSettingsConfigFullPath)"
    Query="//settings/web.routing/@umbracoApplicationUrl"
    Value="$(UmbracoApplicationUrl)" 
    Condition="$(__SetUmbracoApplicationUrl)" />

Таким, довольно хитрым образом, я упростил себе развертывание новых проектов и убрал потенциальную ошибку из лога.