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 Teamcity level configurations of umbraco-driven Continuous Delivery (cd) projects.
Umbraco is an open-source ASP.net CMS 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:
- 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)
- 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 msbuild 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:
$(TargetsTriggeredByCompilation); UmbracoSettingsConfigTransform;
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:
False <__SetScheduledTasksBaseUrl>!$(DoNotSetScheduledTasksBaseUrl)False <__SetUmbracoApplicationUrl>!$(DoNotSetUmbracoApplicationUrl)
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:
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 MsBuild 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 MsBuild 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.MSBuild.CustomTasks.dll"/> <Target Name="EstablishConfigFileModificationsRequirements"> <!-- Get version from web.config --> <XmlPeek Namespaces="<Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/>" 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 || >=7.1.9 <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=">=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:
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
Final task is to use the system.website.hostname Teamcity variable and value of the umbracoPath key from web.config, to build the URL to access Umbraco with the help of an additional MsBuild 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 MsBuild XmlPoke tasks:
<XmlPoke Namespaces="<Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/>" XmlInputPath="$(UmbracoSettingsConfigFullPath)" Query="//settings/scheduledTasks/@baseUrl" Value="$(BaseUrl)" Condition="$(__SetScheduledTasksBaseUrl)" /> <XmlPoke Namespaces="<Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/>" XmlInputPath="$(UmbracoSettingsConfigFullPath)" Query="//settings/web.routing/@umbracoApplicationUrl" Value="$(UmbracoApplicationUrl)" Condition="$(__SetUmbracoApplicationUrl)" />
So, here it is!