Unicorn Content Artifact in Azure DevOps

Unicorn Content Artifact in Azure DevOps

Adhering to Sitecore Helix conventions and practices makes a Sitecore solution easier to maintain as the modular architecture allows for good dependency management.

As one of its guidelines, Helix also lays out the directory structure of serialized files whether using TDS or Unicorn with the latter being the focus of this blog post. In short, the structure should look like this:

/[Solution directory]
    /src
        /[Foundation|Feature|Project]
            /[Module Name]
                /serialization
                    /[Unicorn Predicate Name]        // e.g. Templates
                    /[Unicorn Predicate Name]        // e.g. Renderings
                    /[Unicorn Predicate Name]        // e.g. Branches

The proposed layout makes it easy for developers to check-in items into a repository where they are neatly organised and stored near the code they belong to. But what about environments where the source code and solution files are not present on the machine, e.g. Sitecore instances hosted in a PaaS model? There is no src directory there, just the wwwroot directory containing the website.

In those cases, Unicorn's YAML files should be stored somewhere within the web root. The App_Data is a good candidate as it is the traditional place to store file-based data stores. What's more, without any additional configuration, Unicorn defaults to $(dataFolder)/Unicorn where $dataFolder is a Sitecore setting set to /App_Data by default.

This article present two ways of deploying Unicorn content files using Azure DevOps.

Option One

In this option, the serialized items will be published alongside the project to be further packaged or deployed to a website directly. To achieve that, a new target has to be added in the project file:

<Target Name="CopyUnicorn" AfterTargets="CopyAllFilesToSingleFolderForPackage">
    <ItemGroup>
        <ContentFiles Include="..\serialization\**\*.*" />
    </ItemGroup>
    <PropertyGroup>
      <AssemblyNameSegmentsCount>$(AssemblyName.Split(".").Length)</AssemblyNameSegmentsCount>
      <ModuleLayer>$(AssemblyName.Split(".").GetValue($([MSBuild]::Subtract($(AssemblyNameSegmentsCount), 2))))</ModuleLayer>
      <ModuleName>$(AssemblyName.Split(".").GetValue($([MSBuild]::Subtract($(AssemblyNameSegmentsCount), 1))))</ModuleName>
    </PropertyGroup>
    <Message Importance="High" Text="Copying Unicorn serialization files to temporary location for package/publish" />
    <Copy SourceFiles="@(ContentFiles)" DestinationFolder="$(_PackageTempDir)\App_Data\Unicorn\$(ModuleLayer)\$(ModuleName)\%(RecursiveDir)" />
</Target>

This snippet ensures that the contents of the serialization directory are copied to the temporary location, from which the packaging/deployment happens.

Pasting the same configuration into all project files is not very maintainable. A better solution is to create a Unicorn.targets file in the root of the Solution directory with the following contents:

<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="CopyUnicorn" AfterTargets="CopyAllFilesToSingleFolderForPackage">
    <ItemGroup>
        <ContentFiles Include="..\serialization\**\*.*" />
    </ItemGroup>
    <PropertyGroup>
      <AssemblyNameSegmentsCount>$(AssemblyName.Split(".").Length)</AssemblyNameSegmentsCount>
      <ModuleLayer>$(AssemblyName.Split(".").GetValue($([MSBuild]::Subtract($(AssemblyNameSegmentsCount), 2))))</ModuleLayer>
      <ModuleName>$(AssemblyName.Split(".").GetValue($([MSBuild]::Subtract($(AssemblyNameSegmentsCount), 1))))</ModuleName>
    </PropertyGroup>
    <Message Importance="High" Text="Copying Unicorn serialization files to temporary location for package/publish" />
    <Copy SourceFiles="@(ContentFiles)" DestinationFolder="$(_PackageTempDir)\App_Data\Unicorn\$(ModuleLayer)\$(ModuleName)\%(RecursiveDir)" />
</Target>
</Project>

Then only the line <Import Project="..\..\..\..\Unicorn.targets" /> has to be added to each project file. Maintainable and clean!

By using this method, the content files will be in the same artifact as the code (read my article about creating code artifacts here).

Option Two

Another way to approach the subject is to "extract" the content files and create a separate build artifact containing just content.

Here is an example script for extracting the serialized items:

$layers = @("Foundation", "Feature", "Project")

foreach ($layer in $layers) {
    if (Test-Path ".\Sitecore\src\$layer") {
        $projects = Get-ChildItem ".\Sitecore\src\$layer" |? { Test-Path "$($_.FullName)\serialization" }
        foreach ($project in $projects) {
            robocopy /S "$($project.FullName)\serialization" "$env:ContentArtifactDir\$env:ContentDeployPath\$layer\$($project.Name)\serialization"
            if ($LASTEXITCODE -ge 2) {
                throw ("An error occured while copying. [RoboCopyCode: $($LASTEXITCODE)]")
            }
            else {
                $global:LASTEXITCODE = 0;
            }
        }
    }
}

exit $LASTEXITCODE

The script requires two variables to be set in the build pipeline to work correctly:

  • ContentArtifactDir - if the content files are not modified any further it is good to just set the variable to $(Build.ArtifactStagingDirectory), which is set by Azure DevOps
  • ContentDeployPath - specifies where the files should be stored on the app service, if the default paths are used, this should be set to App_Data\Unicorn

Producing the build artifact can be done using the Publish build artifacts task as shown here.

Deploying the content

When deploying the artifact make sure to use the Web Deploy deployment method and uncheck the Exclude files from the App_Data folder if you choose to store the serialized items in it.

Synchronising the content

By using the Sync Unicorn PowerShell script, the content can be automatically synchronised after each deploy.

Cover photo by Meritt Thomas on Unsplash

Show Comments