April 13, 2019 · How-To

Automating Chocolatey Package Development With Azure DevOps

As we began to roll out Chocolatey in our organization, we realized we needed to ensure consistency in the process of package creation and distribution to clients. To do this, we utilized Chocolatey package templates, custom tests for the .nuspec and chocolateyinstall.ps1, and some custom code to copy binaries down from our file share for the final choco pack and choco push steps. The best method we found to automate this turned out to be Azure DevOps's excellent CI/CD feature set.

The Chocolatey Package Template

We started by creating the package template. Our needs were very minimal, and the template we created reflected that. Here's the entire .nuspec:

<?xml version="1.0" encoding="utf-8"?>

<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">

  <metadata>

    <id>_REPLACE_</id>
    <version>_REPLACE_</version>

    <!-- == SOFTWARE SPECIFIC SECTION == -->
    <!-- This section is about the software itself -->

    <title>_REPLACE_</title>
    <authors>_REPLACE_</authors>
    <summary>_REPLACE_</summary>
    <description>_REPLACE_</description>

  </metadata>

  <files>
    <!-- Don't touch anything below this line -->
    <file src="tools\**" target="tools" />
  </files>

</package>

After creating the simple .nuspec, we customized the standard chocolateyinstall.ps1 to include a section like this (we want to embed the binaries inside our Chocolatey packages, ensuring that when choco install is run on a remote client outside our firewall, they can still install the package):

# To embed the binaries, place them in inside of the tools directory
$fileLocation = Join-Path $toolsDir '_REPLACE_'

# Replace with full name of binary below (example.msi)
$binaryfile = "\\share.yourdomain.com\chocolatey\_REPLACE_"

Writing Some Tests

Validating the Chocolatey packages being uploaded to the internal repository was important to us, so we wrote some tests – one for the metadata and a different check run against the chocolateyinstall.ps1.

Since the template we designed was used, we could ensure that metadata was present by simply checking for the string _REPLACE_.

For the chocolateyinstall.ps1, we decided to start by checking for a specific mistake we'd seen – forgetting to uncomment the necessary silent arguments for the actual installer (the .exe or .msi).

Download Binary For Build Steps

Once the build pipeline was running (after tests had passed) we needed the build agent to be able to download the binary and drop it inside the tools directory before actually building the package. We accomplished this by:

  1. Using a self-hosted build agent in Azure DevOps
  2. Ensuring the path to the binary was inside our chocolateyinstall.ps1 template (see above, in the template section)
  3. Adding a Copy-Item step to copy from that path to the local clone of our Chocolatey package repository

Tying It All Together In Azure DevOps

To keep our build pipeline modular, we broke out these steps into individual Azure Pipelines templates, and stored these templates inside a "build tools" repository separate from the main Chocolatey package mono-repo.

The build pipeline runs on a commit to any pkg/ branch name, and the pipeline keys on the branch name to know what package directory to run checks and build steps against.

For example, here is the metadata.yml for the _REPLACE_ check:

steps:
- powershell: |
    $packagestocheck = Get-ChildItem -Path "$Env:BUILD_SOURCESDIRECTORY" -Recurse -Include *.nuspec,*.ps1
    foreach ($pkg in $packagestocheck){
        $metadatastatus = Select-String -Path $pkg -Pattern '_REPLACE_'
        if (!$metadatastatus){
            Write-Output "All metadata is valid for $pkg."
        }
        else{
            Write-Error -Message "String _REPLACE_ is still in $pkg. Please input vaild data on the following lines: `n $metadatastatus."
        }
    }
    
  displayName: 'Check for _REPLACE_'
  errorActionPreference: stop

And here is how it all comes together in the main Chocolatey package repo's azure-pipelines.yml:

# This build pipeline tests and builds Chocolatey packages pushed to any branch called pkg/* 
# Built packages are pushed to the internal Chocolatey server.

trigger:
- pkg/*
- pkgupdate/*

pool:
  name: Default

resources:
  repositories:
    - repository: buildtools
      type: github
      name: nameofrepo

steps:
- template: [email protected]  
- template: [email protected]
- template: [email protected]
- template: [email protected]

Assuming all of the tests pass, the resulting compiled nupkg is pushed to the internal Chocolatey package repository and is then available for clients to install!

If you're interested in more detail on how some of this came together or how we integrated this workflow into infrastructure that mostly exists on-premise, you can find me over on the #chocolatey channel in the MacAdmins Slack. Happy automating!