Continuous Delivery with Squirrel and VSTS

Imagine this scenario, you find a serious bug that needs to get fixed NOW! But, how can you push that update to your customers quickly?

Answer: Continuous Delivery.

This blog post will walk-through setting up a continuous delivery pipeline using Squirrel.Windows and Visual Studio Team Services (VSTS) such that Desktop Windows applications will auto-update itself given a new release.

Continuous Delivery

The goal is the following:

  1. Push changes to the Git repo
  2. Triggers a build on Visual Studio Team Services to build and test changes.
  3. Releases new version
  4. The application notices the new release and updates itself.

In order to accomplish this, we're going to use Squirrel.Windows and Visual Studio Team Services.

The Tools

Squirrel

Squirrel for Windows is an open source project and offers a set of tools and libraries that can be used to create custom installer and update frameworks for Windows Desktop applications.

Visual Studio Team Services

Visual Studio Team Services (VSTS) is a one-stop shop for managing your software project. On VSTS, we'll setup a custom build definition to push new releases to an update server.

Integrate Squirrel

I will defer to Squirrel's documentation on how to integrate a Squirrel updater into your own application. For reference, this is how I did it in my own app:

// Updater.cs
public class Updater  
{
    private static readonly string UpdatePath = ConfigurationManager.AppSettings["updateDropLocation"];

    private static int UpdateCheckFrequencyInMs
    {
        get
        {
            return int.Parse(ConfigurationManager.AppSettings["updateCheckFreqencyInMinutes"]) * 60 * 1000;
        }
    }

    public static Task Create(CancellationToken token)
    {
        return new Task(() =>
        {
            if (!CanUpdate())
            {
                return;
            }

            while (true)
            {
                if (token.IsCancellationRequested)
                {
                    return;
                }

                Updater.CheckAndApplyUpdates();
                Thread.Sleep(UpdateCheckFrequencyInMs);
            }
        }, token, TaskCreationOptions.LongRunning);
    }

    private static void CheckAndApplyUpdates()
    {
        try
        {
            bool shouldRestart = false; 
            using (var mgr = new UpdateManager(UpdatePath))
            {
                var updateInfo = mgr.CheckForUpdate().Result;
                if (updateInfo.CurrentlyInstalledVersion.Version < updateInfo.FutureReleaseEntry.Version)
                {
                    shouldRestart = true;
                    mgr.DownloadReleases(updateInfo.ReleasesToApply).Wait();
                    mgr.ApplyReleases(updateInfo).Wait();
                }
            }

            if (shouldRestart)
            {
                UpdateManager.RestartApp();
            }
        }
        catch (Exception e)
        {
            // Uh Oh!
        }
    }

    private static bool CanUpdate()
    {
        return !string.IsNullOrWhiteSpace(UpdatePath);
    }
}

With the configurations set to:

// app.config
<configuration>  
  <appSettings>
    <add key="updateCheckFreqencyInMinutes" value="60" />
    <add key="updateDropLocation" value="http://{your_storage_account}.blob.core.windows.net/{public_container}" />
  </appSettings>
</configuration  

and here's how I used it.

// Program.cs
static int Main(string[] args)  
{
    ...

    var updateTask = Updater.Create(cts.Token);
    updateTask.Start();

    ...
}                

Creating an Update Package

Squirrel uses NuGet for bundling application files into a release package. We'll use a .nuspec to define what files to include in the package; you should end up with something similar to this:

<?xml version="1.0" encoding="utf-8"?>  
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">  
    <metadata>
        <id>{Your App Name}</id>
        <version>$version$</version>
        <authors>{Your Name}</authors>
        <requireLicenseAcceptance>false</requireLicenseAcceptance>
        <description>{Some Description}</description>
    </metadata>
    <files>
        <file src="bin\x86\Release\*.dll" target="lib\net45\"></file>
        <file src="bin\x86\Release\*.exe" exclude="**\*.vshost.exe" target="lib\net45\"></file>
        <file src="bin\x86\Release\*.config" target="lib\net45\"></file>
    </files>
</package>  

Given the .nuspec, we can create a .nupkg, at which point, Squirrel.exe can be used to prepare the .nupkg for distribution.

PM> Squirrel --releasify {My_Package.1.0.0}.nupkg  

Full details are described here.

Setting up the Build

At this point, we should be able to manually create installer packages. We'll use VSTS to automate all these steps. In addition to the usual build and test steps, we'll add a couple of custom build tasks:

1. Set Assembly Version

With each build, it's good practice to increment the assembly version. The easiest way of doing this is mentioned in this SO post. However, I wanted a more intuitive build version using the build date, for example: 2016.01.01.1. I accomplished by sharing assembly info and updating SharedAssemblyInfo.cs with the following PowerShell script.

# Look for a 0.0.0.0 pattern in the build number. 
# If found use it to version the assemblies.
#
# For example, if the 'Build number format' build process parameter 
# $(BuildDefinitionName)_$(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r)
# then your build numbers come out like this:
# "Build HelloWorld_2013.07.19.1"
# This script would then apply version 2013.07.19.1 to your assemblies.

# Regular expression pattern to find the version in the build number 
# and then apply it to the assemblies
$VersionRegex = "\d+\.\d+\.\d+\.\d+"

# Inspired by https://msdn.microsoft.com/en-us/library/vs/alm/build/scripts/index

# Make sure there is a build number
if (-not $Env:BUILD_BUILDNUMBER)  
{
    Write-Error ("BUILD_BUILDNUMBER environment variable is missing.")
    exit 1
}
Write-Output "BUILD_BUILDNUMBER: $Env:BUILD_BUILDNUMBER"

# Get and validate the version data
$VersionData = [regex]::matches($Env:BUILD_BUILDNUMBER,$VersionRegex)

switch($VersionData.Count)  
{
   0        
      { 
         Write-Error "Could not find version number data in BUILD_BUILDNUMBER."
         exit 1
      }
   1 {}
   default 
      { 
         Write-Warning "Found more than instance of version data in BUILD_BUILDNUMBER." 
         Write-Warning "Will assume first instance is version."
      }
}
$NewVersion = $VersionData[0]

Write-Output  "Updating shared assembly info..."  
$file = ".\{Your_Location_of}\SharedAssemblyInfo.cs" 

if($file)  
{
    $filecontent = Get-Content($file)
    attrib $file -r

    # Search in the "SharedAssemblyInfo.cs" file items that matches the version regex and replace them with the
    # correct version number
    $filecontent -replace $VersionRegex, $NewVersion | Out-File $file
    Write-Output "$file.FullName - version updated to $NewVersion"
}
else  
{
    Write-Warning "Found no files."
}

2. Create NuGet package

This step will create NuGet packages given .nuspec files in our repository.

3. Downloading Prior Packages

In addition to generating full packages, Squirrel also creates delta file packages to reduce the package size. In order to do so, it needs the previously generated packages. We'll use an Azure Powershell build task with the following script to download prior releases to the build machine:

[CmdletBinding()]
param(  
    [Parameter(Mandatory)][string] $containerName,
    [Parameter(Mandatory)][string] $destinationFolder,
    [Parameter(Mandatory)][string] $connectionString
)

$storage_account = New-AzureStorageContext -ConnectionString $connectionString
$blobs = Get-AzureStorageBlob -Container $containerName -Context $storage_account
Write-Output  "Found $($blobs.count) blob(s)"

$destinationPath = $(Join-Path (Get-Item $PSScriptRoot ).parent.parent.FullName $destinationFolder)

$directory = New-Item -ItemType Directory -Force -Path $destinationPath
Write-Output  "Downloading to $directory"

foreach ($blob in $blobs)  
{
    $blobContent = Get-AzureStorageBlobContent `
        -Container $containerName `
        -Blob $blob.Name `
        -Destination $destinationPath `
        -Context $storage_account `
        -Force

    Write-Output "$($blobContent.Name)"
}   

The script takes 3 arguments:

  • containerName - Container where the Squirrel update packages are located. Ensure that this corresponds with {public_container} that you set above in the app.config of your Windows application.
  • destinationFolder - Destination folder on local build machine to download the packages
  • connectionString - Azure connection string. Should be of the form: DefaultEndpointsProtocol=https;AccountName={your_storage_account_name};AccountKey={your_storage_key}

4. Creating Squirrel Package

Squirrel.exe is included in the Squirrel.Windows NuGet package. This PowerShell build task will look for the tool run --releasify against the generated .nupkg.

[CmdletBinding()]
param(  
    [Parameter(Mandatory)][string] $nugetSearchPath
)

# Find squirrel.exe
$squirrel = gci -Recurse -Filter *.exe | ?{ !$_.PSIsContainer -and [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -eq "Squirrel"} | % { $_.FullName }
if(!$squirrel)  
{
    Write-Error "Unable to find Squirrel.exe"
    exit 1
}

Write-Output "Found Squirrel Installation. Using $squirrel"

# Find nupkg
$nupkgs = gci $nugetSearchPath -Recurse -Filter *.nupkg | ?{ !$_.PSIsContainer } | % { $_.FullName }
if(!$nupkgs)  
{
    Write-Warning "Unable to find nupkg(s)"
    exit 1
}

Write-Output  "Found $($nupkgs.count) nupkg(s)"  
foreach ($nupkg in $nupkgs) {  
    Write-Output  $nupkg

    # Crete squirrel package from nupkg
    &$squirrel --releasify $nupkg | Write-Output
}

The script takes one argument:

  • nugetSearchPath - The script will recursively search for any .nupkg in the given search path. Ensure that this corresponds with the .nupkg's created in step 2.

5. Copying build assets to Azure

Finally, once the Squirrel packages have been created, upload them to Azure with an Azure File Copy build task. Ensure that the container access is set to public.

Boom!

With each successful build, you should see your application download and install the new release and restart itself.