Tech Blog Menu

Continuous Integration of a Sitecore Project Using AppVeyor

By

Continuous integration (CI) is the act of automatically compiling code and running its tests every time a change is made. It is an important step in a project to ensure quality and save time. It needs to be implemented before continuously deploying an application.

In this post, I will explore how to configure CI with AppVeyor using the Sitecore Habitat demo project.

AppVeyor overview

AppVeyor is a CI cloud service. It is free for public repositories. One of its key feature is the support of Windows, .NET, Visual Studio, and MSBuild. It is a perfect tool for a Sitecore project.

Creating a project

AppVeyor supports signing in with your VSTS, Bitbucket, or GitHub account. Once logged in, you can create a project for any of your repositories. It supports a wide range of repository sources. Here, I chose my GitHub Habitat fork.

Add an AppVeyor project from GitHub

Configuring the project

AppVeyor has a complete settings user interface, but it alternatively supports storing the project configuration in an appveyor.yml file at the root of your repository. I recommend the configuration file for many reasons:

  • It provides a complete view of all the configuration, so you do not have to navigate the various UI sections.
  • It is always accessible, even when you work offline.
  • Every time a modification to the file is pushed, your project is automatically built.
  • It allows contributors to continuously integrate their work using AppVeyor when they fork your project.

You should note that the file is not merged with the settings UI, except for the environment variables and notification settings. The build version format is taken from the UI if it is not set in the file.

The configuration file requires only a few settings to get started:

branches:
  only:
    - master

image: Visual Studio 2017

build:
  project: Habitat.sln

In the example above, I specified that I want to compile the Habitat.sln solution on an environment with Visual Studio 2017 and to only run for the master branch commits.

Running the AppVeyor project with this configuration unveils a first problem.

Build started
git clone -q --branch=master https://github.com/jflheureux/Habitat.git C:\projects\habitat
git checkout -qf 23c3002eceffe483e2b46df74f040668d1c00193
msbuild "C:\projects\habitat\Habitat.sln" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
Microsoft (R) Build Engine version 15.3.409.57025 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.
Building the projects in this solution one at a time. To enable parallel build, please add the "/m" switch.
Build started 9/21/2017 2:30:34 PM.
Project "C:\projects\habitat\Habitat.sln" on node 1 (default targets).
ValidateSolutionConfiguration:
  Building solution configuration "Debug|Any CPU".
Project "C:\projects\habitat\Habitat.sln" (1) is building "C:\projects\habitat\src\foundation\SitecoreExtensions\code\Sitecore.Foundation.SitecoreExtensions.csproj" (2) on node 1 (default targets).
PrepareForBuild:
  Creating directory "bin\".
  Creating directory "obj\Debug\".
Project "C:\projects\habitat\src\foundation\SitecoreExtensions\code\Sitecore.Foundation.SitecoreExtensions.csproj" (2) is building "C:\projects\habitat\src\Foundation\DependencyInjection\code\Sitecore.Foundation.DependencyInjection.csproj" (3:2) on node 1 (default targets).
C:\projects\habitat\src\foundation\DependencyInjection\code\Sitecore.Foundation.DependencyInjection.csproj(155,5): error : This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is ..\..\..\..\packages\Microsoft.Net.Compilers.1.3.2\build\Microsoft.Net.Compilers.props. [C:\projects\habitat\src\Foundation\DependencyInjection\code\Sitecore.Foundation.DependencyInjection.csproj]
_CleanRecordFileWrites:
  Creating directory "obj\Debug\".
Done Building Project "C:\projects\habitat\src\Foundation\DependencyInjection\code\Sitecore.Foundation.DependencyInjection.csproj" (default targets) -- FAILED.
Done Building Project "C:\projects\habitat\src\foundation\SitecoreExtensions\code\Sitecore.Foundation.SitecoreExtensions.csproj" (default targets) -- FAILED.

...

    18 Warning(s)
    23 Error(s)

Time Elapsed 00:00:11.54
Command exited with code 1

Restoring NuGet packages

Habitat relies on public NuGet packages from NuGet.org and from the Sitecore NuGet feed. These sources are configured in the nuget.config file of the Habitat repository and NuGet automatically handles this file. To restore the packages in AppVeyor, you must add a command line instruction to run in the before_build section:

before_build:
  - nuget restore Habitat.sln

Caching

The NuGet packages will rarely change in your project. To speed up the builds, it is recommended to cache the packages between builds. This is done in the cache section:

cache:
  - packages -> **\packages.config  # preserve NuGet packages in the root of build folder but will reset it if any packages.config file is modified
  - '%LocalAppData%\NuGet\Cache'    # NuGet < v3
  - '%LocalAppData%\NuGet\v3-cache' # NuGet v3

This time, the build is successful but the unit tests are failing.

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:02:33.21
Discovering tests...OK
%xunit20%\xunit.console.x86 "C:\projects\habitat\src\Feature\Accounts\Tests\bin\Debug\Ploeh.AutoFixture.Xunit2.dll" -appveyor
xUnit.net Console Runner (32-bit .NET 4.0.30319.42000)
System.IO.FileLoadException: Could not load file or assembly 'xunit.core, Version=2.0.0.2929, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
Command exited with code 1

Choosing tests

AppVeyor automatically tries to detect assemblies containing unit tests. In some cases, you must help it in the test section:

test:
  assemblies:
    only:
      - \src\**\Tests\bin\**\Sitecore.*.Tests.dll

Habitat has a very good naming convention that helps you configure the list of assemblies to test. When running the build, the unit tests now complain about the missing Sitecore license.

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:02:31.75
Discovering tests...OK
%xunit20%\xunit.console.x86 "C:\projects\habitat\src\Feature\Accounts\Tests\bin\Debug\Sitecore.Feature.Accounts.Tests.dll" -appveyor
xUnit.net Console Runner (32-bit .NET 4.0.30319.42000)
  Discovering: Sitecore.Feature.Accounts.Tests
  Discovered:  Sitecore.Feature.Accounts.Tests
  Starting:    Sitecore.Feature.Accounts.Tests

    Sitecore.Feature.Accounts.Tests.Attributes.RedirectAuthenticatedAttributeTests.OnActionExecuting_AuthenticatedUser_ShouldRedirect(db: master, item: [], filterContext: ActionExecutingContextProxy { __interceptors = [CastleForwardingInterceptor { ... }], __mixin_NSubstitute_Core_ICallRouter = CallRouter { }, ActionDescriptor = ActionDescriptorProxy { __interceptors = [...], __mixin_NSubstitute_Core_ICallRouter = CallRouter { ... }, ActionName = "", ControllerDescriptor = ControllerDescriptorProxy { ... }, UniqueId = "" }, ActionParameters = [], Controller = null, ... }, redirectAuthenticatedAttribute: RedirectAuthenticatedAttribute { AllowMultiple = False, Order = 185, TypeId = typeof(Sitecore.Feature.Accounts.Attributes.RedirectAuthenticatedAttribute) }) [FAIL]
      Sitecore.SecurityModel.License.LicenseException : Required license is missing: Runtime

The Sitecore license

Since you do not want to share your Sitecore license to the rest of the world, you must not store it in your repository. AppVeyor’s solution to manage private files is to commit an encrypted version and to add a decryption step to the install section, before the project is built:

install:
  - nuget install secure-file -ExcludeVersion
  - secure-file\tools\secure-file -decrypt .\lib\license.xml.enc -secret "%LicenseEncryptionKey%"

Secure-file is an AppVeyor tool to encrypt and decrypt files using a private encryption key. Using it is simple but tricky.

Encryption key

  • Choose a long enough key for your files to be secure.
  • Choose a key shorter than 32k characters as it will be stored as an environment variable. There are only 32k characters available to store all the environment variables on Windows. Be considerate.
  • Avoid special characters like / \ | ~ % < > ` ' " in your encryption key as it will be passed as a command line argument.
  • Avoid the ^ character as AppVeyor will strip it from your environment variable.

Environment variable

The encryption key must be stored securely in AppVeyor and not be visible in the build logs. Environment variables are perfect for this. It is one of the only setting that is merged between the UI and the configuration file.

Add an AppVeyor project from GitHub

Here, I chose to name my environment variable LicenseEncryptionKey. You have to set its value.

Installing on your local computer

As you need to encrypt your files locally, you need the tool as well. Run this command in a console:

nuget install secure-file -ExcludeVersion

Encryption

The Habitat gulp script copies the Sitecore license file from your webroot to the lib folder of the devroot.

Encrypt your Sitecore license file using the following command. The location of the file may vary. Replace YOUR_ENCRYPTION_KEY by the key you defined earlier.

secure-file\tools\secure-file -encrypt .\lib\license.xml -secret "YOUR_ENCRYPTION_KEY"

Moment of truth

Commit and push the resulting license.xml.enc file. Your unit tests should now run successfully.

...

%xunit20%\xunit.console.x86 "C:\projects\habitat\src\Foundation\Theming\tests\bin\Debug\Sitecore.Foundation.Theming.Tests.dll" -appveyor
xUnit.net Console Runner (32-bit .NET 4.0.30319.42000)
  Discovering: Sitecore.Foundation.Theming.Tests
  Discovered:  Sitecore.Foundation.Theming.Tests
  Starting:    Sitecore.Foundation.Theming.Tests
  Finished:    Sitecore.Foundation.Theming.Tests
=== TEST EXECUTION SUMMARY ===
   Sitecore.Foundation.Theming.Tests  Total: 12, Errors: 0, Failed: 0, Skipped: 0, Time: 3.742s
Updating build cache...
Cache 'packages' - Up to date
Cache 'C:\Users\appveyor\AppData\Local\NuGet\Cache' - Up to date
Cache 'C:\Users\appveyor\AppData\Local\NuGet\v3-cache' - Up to date
Build success

Improving build performance

By default, the log level is very verbose. To improve build time, you can set the build verbosity in the build section:

build:
  verbosity: minimal

If the build fails in the future and the logs are not verbose enough, you can temporarily comment this line.

Final configuration file

branches:
  only:
    - master

image: Visual Studio 2017

cache:
  - packages -> **\packages.config  # preserve NuGet packages in the root of build folder but will reset it if any packages.config file is modified
  - '%LocalAppData%\NuGet\Cache'    # NuGet < v3
  - '%LocalAppData%\NuGet\v3-cache' # NuGet v3

install:
  - nuget install secure-file -ExcludeVersion
  - secure-file\tools\secure-file -decrypt .\lib\license.xml.enc -secret "%LicenseEncryptionKey%"

before_build:
  - nuget restore Habitat.sln

build:
  project: Habitat.sln
  verbosity: minimal

test:
  assemblies:
    only:
      - \src\**\Tests\bin\**\Sitecore.*.Tests.dll

Packaging build artifacts

When a build is successful, AppVeyor can package the artifacts and publish them in its cloud storage. I have yet to try this feature. It will be part of a separate post.

Conclusion

Before using AppVeyor, I tried to setup another local continuous integration tool. After countless hours, it was still not working. I was discouraged and had hard feelings towards CI.

Needless to say I was happy and impressed by AppVeyor. It took me under 4 hours to learn the basics and have my first successful build of the Sitecore Habitat project. I really like the product as it has a low barrier of entry and is free for public repositories. I plan to use it for a more complex Sitecore project in the near future. Stay tuned as I share my experiences as I progress.

If you believe continuous integration is hard, I encourage you to try AppVeyor and experience how easy it can be.