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

Automated umbracoSettings.config modifications

This is one more piece of automation puzzle to save time and make the life of a build engineer easier. This part is targeted towards removing double work of redoing things that are already done on   level configurations of Umbraco-driven Continuous Delivery (CD) projects.

Umbraco is an open-source ASP.NET with a big developers community. One of the features of this CMS, which makes automated deliveries somewhat harder to configure for build engineer, is a configuration file system, which consists out of web.config and bunch of other .config files, stored in the ~/config directory. This is brilliant idea - each Umbraco part is driven by its own configuration file, but it becomes a nightmare when you have to setup some environment-specific settings in some of these files. Configuration file transformation, even in Visual Studio 2015, allows you to build web.config file transforms only. Of course, there is the SlowCheetah plugin, which alleviates this problem, but it generates extra work for the build engineer. Since we have an established automated Continuous Delivery process, our main target is to remove all those small distractive pieces of configuration and automate everything J.

In this post I will cover automated modifications on one of the two parts of the umbracoSettings.config file, responsible for KeepAliver, task scheduling and delayed publishing in Umbraco.

To my great grieve, Umbraco has two of such parameters, which are version dependent:

  1. for versions 6.2.5 and 7.1.9-7.2.7 it is the baseUrl attribute of the scheduledTasks element (since 7.2.7 it is obsolete, in previous versions it is not present)
  2. since 7.2.7 it is the umbracoApplicationUrl attribute of the web.routing element

Target of both: remove automated guess from first request about Umbraco backoffice access URL and set it up by configuration (fully, they are covered at https://our.umbraco.org/documentation/reference/config/umbracosettings/ and http://issues.umbraco.org/issue/U4-6788).

Code and explanations

First of all, we have to modify the compilation process to make sure that this task is executed directly after compilation and before configuration files transformation, by adding it to the centralized project file PropertyGroup element:

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

This will ensure that our target UmbracoSettingsConfigTransform is called directly after compilation.  Next to it, as flexibility is required, in the same PropertyGroup we add some variables, which allows to stop automated transformation:

<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>

As one can see, if the variables DoNotSetScheduledTasksBaseUrl and DoNotSetUmbracoApplicationUrl are not set, they are made False, thus allowing further transforms. If variables are set, the build process will receive the corresponding values. After that, the build process will copy the original, unmodified version to temporary storage. After the build is finished, the unmodified version will be placed back to alleviate possible version control system (VCS) checkout issues:

<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>

Next to it, we have to check, if one of 2 possible modifications is valid for the current Umbraco version. Umbraco stores the currently installed version in web.config, in the appSettings section, in the value of a key umbracoConfigurationStatus. So, on build, code is retrieving this value using XmlPeek task, then, using Nuget package SemanticVersioning (https://www.nuget.org/packages/SemanticVersioning/,https://github.com/adamreeve/semver.net) version is compared against the range using custom tasks and the internal variable receives True or False value (to allow or disallow modification):

<UsingTask TaskName="CompareSemanticVersions" AssemblyFile="$(TeamCityCiToolsPath)\CI.Builds\MsBuildCustomTasks\Colours.Ci..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>

I wish to focus on custom task:

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

Actually, it is just a code, compiled to assemble. It receives the current version in CurrentVersion parameter, a range of allowed versions in AllowedVersionsRange parameters in NPM notation (https://github.com/npm/node-semver#ranges) and outputs Boolean variable. The same task is called to check version requirements for the umbracoApplicationUrl parameter. There is one feature which shall be known - characters ‘<’ and ‘>’ must be escaped. The symbol ‘<’ is represented as ‘&lt;’. The symbol ‘>’ is represented as ‘&gt;’. After checking versions, we have to modify umbracoSettings.config by using the XmlTransform task, as (surprise!!!) XmlPoke tasks, used to add URL to attribute could not insert missing attributes. So, it was added static transform files, which just adds attributes with an ‘empty’ value:

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

Final task is to use the system.website.hostname variable and value of the umbracoPath key from web.config, to build the URL to access Umbraco with the help of an additional task. I have to build such a cumbersome solution, because there are differences between how attribute values shall look:

  • baseUrl shall not contain schema, but port (e.g. it shall be something like mysite.com:80/Umbraco)
  • umbracoApplicationUrl shall be just fully qualified domain name (FQDN) - (http://mysite.com/umbraco)

It is done by using 2 built-in XmlPoke tasks:

<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)" />

So, here it is!