Publish a .NET Console App to Chocolatey using GitHub Actions
In my previous post Providing quality documentation in your project with DocFx and Companion Tools I explained how we created companion tools to help you validate markdown documentation and to generate a table of contents for use with DocFx.
One of the ‘problematic’ parts of the solution, was that you had to copy over (or fork) the sources of the tools for use. Of course that brings a lot of issues with it as well, especially when updates occur on the tools. This brought me to investigating time publishing the tools to Chocolatey for easy reuse binaries in a pipeline (or locally) on Windows. So the basic question was: how to get a .NET Console Application into Chocolatey.
There is lots of information on Chocolatey and how to publish, but it took me some while to get to a solution for this simple scenario. That’s why I published this in this post 🤓.
What to distribute?
I already mentioned it was about a .NET Console App. But when you compile a Console Application, you see a folder with a lot of DLL’s, the EXE and other files. For distribution, I like a more cleaner model. So I used the option publishing an executable as a single exe, which is possible from .NET 5 and higher. I used this dotnet command for this purpose:
dotnet publish MySoltutionName -c Release -r win-x64 /p:PublishSingleFile=true /p:CopyOutputSymbolsToPublishDirectory=false — self-contained false -o ./output
When you publish a single file, you must provide the -r parameter to indicate the platform and architecture you want to build for. As Chocolatey is a PowerShell based tool, I just published the win-x64 version of the exe. I also made it depending on a .NET runtime being available on the machine, so I don’t have to include it which would make the exe much larger.
Publishing to Chocolatey requires a version number. So I had to come up with a versioning mechanism to provided a version-number for each release. I selected GitVersion for this. Simply put, it uses the git history including labels to calculate the version number when building. There are many forms you can use, but I selected the MajorMinorPatch type (e.g. 0.2.1). The version increment can be specified, and I defined that pushing into the main branch would increase the minor version. Part of the configuration for git version in .\GitVersion.yml to do that:
Git version also looks at the existing labels in the git history. You can add a label ‘1.0.0’ if you want to skip to that version. Git version will start incrementing from that version.
Another thing that makes it more informative for the user of the Chocolatey package, is to provide release notes with each version. For this purpose I started using labels with PR’s to main to indicate what the purpose of that PR is. I chose to use the feature and enhancement as features, fix and bug as fixes and documentation as a documentation change.
This means that if you don’t add any of these labels to a PR, it’s just not showing up in the release notes. Which could be fine for some changes. That’s up to you to decide.
I used mikepenz/release-changelog-builder-action@v1 from the GitHub Action marketplace. That is doing all the magic of compiling release notes with a settings file. An example of the output:
Now I have versioning and release notes covered, the next step I took was publishing releases in GitHub. The release should get a tag which indicates the version of the release. In the release I want to have a zip-file containing all of the executables I want to distribute.
In my build step I create the single executables as described. I also package up all those executables and a LICENSE file into a zip-file.
To create the release I used the ncipollo/release-action@v1 GitHub Action from the marketplace. That action needs to know the artifacts, in my case a zip-file, the release notes for the version and the tag name (the version).
The latest version is now visible on the start page of your GitHub repo.
When you click it you see the release notes and find the zip-file as download in the Assets section.
Creating the Chocolatey Package
Now it’s time to create the Chocolatey Package. This consists of a few files. You can setup the basics with the choco new command. The two main files that make up the package are:
- A Nuspec file
- The chocolateyinstall.ps1 PowerShell script
The nuspec file is an XML-file with the id of the package, the version, the owner, a description and more. Below a fragment of the nuspec file for the DocFx Companion Tools.
The chocolateyinstall.ps1 script is the PowerShell script that’s being executed on installation on someone’s machine. You can package the executables and such with the Chocolatey package. But you can also create a package that points to an internet location where the package can be found. I chose for that last option, pointing to the zip-file of the release I want to publish. So this install script is used:
The packageName must be the same as the id in the nuspec. The url points to the zip-file in the release folder. The hash is the checksum of the zip-file, so it can’t be spoofed with something else after publication.
In the GitHub workflow we must modify the nuspec and install script to reflect the published zip-file and the version. For this we read the files, make the modifications and save it back. These changes are not commited to the history, as we don’t need to ‘remember’ this. It’s only for the publication process.
I created a release-and-publish workflow which is triggered manually. This way I can determine when I want to publish a new version. You could also have this triggered automatically of course.
Publishing the Chocolatey Package
Now we have the package, we can publish it to Chocolatey. For this purpose you need to create an account with Chocolatey (free). Once you have registered, you also can retrieve your API Key. That is needed for publication.
As the API Key is a secret, we have stored it in the securities of the repository (in Settings) and gave it the name CHOCO_TOKEN. In the pipeline we’ll reference this one for use in the command.
To create the Chocolatey package we’ll use this command:
choco pack <nuspec-file>
This creates the package with the version in the name of the file with the nupkg extension.
Now we need to tell the publish command what the API Key is:
choco apiKey -k <API Key> -source https://push.chocolatey.org/
The source in this case points to the chocolatey standard publishing endpoint. You could also point to your own Chocolatey package endpoint if you’re running that in your own environment.
Now we can publish the package to that location:
choco push <package>
The package is the file with the nupkg extension, which will be uploaded to Chocolatey for processing. The process starts to validate your package. This process is partially automated, but also needs manual moderators to give it the final push. This could take some time, so don’t expect your package to be visible within minutes. In my experience this took between hours and days.
One issue I found is that the Chocolatey package is flagged by the Chocolatey process as one that could contain a virus, but is safe to install. This is weird as I know for sure no virus was included in the package 😯. It turns out that it’s flagged by Cylance. There is a nice post that even ‘Hello World’ apps get flagged like this. So if you are sure no virus is in the package, you can safely ignore this warning.
Installing the package using Chocolatey
Once the package is published, you can install the tools using this command:
choco install docfx-companion-tools
The installation of the DocFx Companion Tools is done by 1) downloading the zip-file 2) validate the checksum (hash) 3) extract the zip-file in the Chocolatey application cache.
Now the executables are accessible from the command line.
It’s a pretty straight-forward process, but the description above took quite some time to get to. I ran into all kinds of issues which are solved in the description above. So I hope this is enough to get you started with this as well.
If you want to inspect all the details of the solution for the DocFx Companion Tools, have a look at the PowerShell scripts .\build.ps1 and .\pack.ps1. The make use of a config and helper script in the .\tools folder. Also have a look at the GitHub workflows in .\.github\workflows folder. All can be found in the Ellerbach/docfx-companion-tools: A suite of tools, pipelines templates to take the very best of DocFX (github.com).