Select Page

There are many articles written on how to create ‘perfect’ continues deployment process with Sitecore, but in my 7 years of Sitecore experience I did not had opportunity to see many functional and complete implementations. The file deployment was never an issue, but synchronous deployment of content(items) and files was.

The main issue that make continues deployment difficult to implement in Sitecore is complexity around deploying items and files in order. I’ve seen different strategies, including deploying with Chief, IIS Web Deploy with Teamcity and TDS deploy, Octopus deployment and many other approaches. Most approached I have  seen came short providing full spectrum of deployment features needed for enterprise, but mainly ability to deploy with one button click and roll back if deployment failed.

I was fortunate enough to have opportunity to work on  continues integration prototype solutions. In this article, I’ll discuss continues deployment of TDS packages.

To have continuous deployment of Sitecore  items we opted to use TDS and UpdateInstallationWizard interface provided by Sitecore.

The process is very simple.  We used msbuild to compile and build TDS packages, the additional script used to collect packages  or package, in our case we used multiple project structure, where each project represented a site or feature, and sequentially deploy each package at a time through custom Service.

The service is designed to import TDS package and log the activity.

 [WebMethod(Description = "Installs a Sitecore Update Package.")]
        public void InstallUpdatePackage(string path)
        {
            // Use custom logger
            var log = Sitecore.Diagnostics.LoggerFactory.GetLogger("PackageLogFileAppender");
            var file = new FileInfo(path);
            if (!file.Exists)
                throw new ApplicationException(string.Format("Cannot access path '{0}'.", path));
            log.Info(path);

            using (new SecurityDisabler())
            {
                var installer = new DiffInstaller(UpgradeAction.Upgrade);
                var view = UpdateHelper.LoadMetadata(path);

                //Get the package entries
                bool hasPostAction;
                string historyPath;
                var entries = installer.InstallPackage(path, InstallMode.Install, log, out hasPostAction, out historyPath);
                installer.ExecutePostInstallationInstructions(path, historyPath, InstallMode.Install, view, log, ref entries);             
            }
        }

 

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>

    <log4net>
      <appender name="InstallerLogFileAppender" type="log4net.Appender.SitecoreLogFileAppender, Sitecore.Logging">
        <file value="$(dataFolder)/logs/AutonationIntaller.{date}.txt" />
        <appendToFile value="true" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%4t %d{ABSOLUTE} %-5p %m%n" />
        </layout>
        <encoding value="utf-8" />
      </appender>
      
      <logger name="InstallerLogFileAppender" additivity="false">
        <level value="INFO" />
        <appender-ref ref="InstallerLogFileAppender" />
      </logger>

    </log4net>

  </sitecore>
</configuration>

Now, we were able to install a package programatically, but one may ask: how would you install more than one package sequentially, without knowing that previous package was deployed successfully, especially if solution is based on multi project solution (like,  Habitat). For this prototype solution, we have created custom log processor. This script makes HTTP call to instantiate install of package and will wait until package installation is complete by checking logs.

#package and push the path contents to OD
function Install-TDS
{
	param(
		[string]$path
	)
    
    Write-Host "---$path---"
	foreach($package in (Get-ChildItem -Path $path))
    {
        $packageName = $package.Name
        Write-Host "Installing $packageName"
        $postParams = @{path="$path\$packageName"}
        
        $retry = $webRetryCount
        while($retry -gt 0)
        {
            try
            {
            Write-Host "Making POST Request: $postParams"
                Invoke-WebRequest -Uri $url -Method POST -Body $postParams -Headers $headers -TimeoutSec $timeout
                $retry = 0

                Write-Host "Attempt: $retry"
            }
            catch
            {
                Write-Host $_.Exception.Message
                $retry = $retry - 1
                if($retry -eq 0)
                {
                    throw $_.Exception.Message
                }
            }
        }
        
        $date = Get-Date
        $date1 = $date | Get-Date -Format "M/d/yyyy"
        $date2 = $date | Get-Date -Format "-h:mm tt"
        $date2length = $date2.Length
        
        $success = -1
        $sw = [system.diagnostics.stopwatch]::startNew()
        while($sw.Elapsed.Seconds -le $timeout)
        {
            $installerName = (Get-ChildItem -Path $datafolder\* -Include *AutonationIntaller* | sort LastWriteTime | select -last 1).Name
            Write-Host "Log File Name: $installerName for $packageName"
            $fileContent = Get-Content $datafolder\$installerName -Raw
            $posmatch = $fileContent -cmatch "(?sm)$packageName((?! ERROR ).)*$date1.{$date2length}: TDS PostDeployActions complete"
            
            if($posmatch)
            {
                #break out of loop the process is done
                Write-Host "Installed Package: $packageName"
                $success = 1
                break
            }

            $negmatch = $fileContent -cmatch "(?sm)$packageName.* ERROR .*$date1.{$date2length}: TDS PostDeployActions complete"
            if($negmatch)
            {
                Write-Host "Error Package: $packageName"
                #if there was en error throw break
                $success = 0
                break
            }

            Start-Sleep -s 3
        }
        $sw.Stop()

        if($success -lt 1)
        {
            if($success -eq -1)
            {
                Write-Host "Package Timeout: $packageName"
            }
            throw [System.Exception] "$packageName failed to install."
        }
    }
} 

 

This approach was created as a prototype and would require addition work to make it ‘production-ready’. For example,  I would like to avoid checking logs for installation completion and have installer method to return true  or false, but it is not organically possible now without extending “installer.ExecutePostInstallationInstructions” class.

I would love to hear if anyone have successfully extended installer.ExecutePostInstallationInstructions to return installation status or made it  in any other way.