I recently started moving some builds from TeamCity, Travis, and AppVeyor to GitHub actions, and while doing that, I thought I would also move to a more straightforward deployment process.
Before, I used to push to NuGet by tagging the main branch, and while this is a perfectly reasonable approach, I wanted to change this to publish previews to NuGet on every push to the main branch. If it's a tagged commit on the main branch, a non-preview version should be published.
To calculate the version number from Git history, I'll use
MinVer by Adam Ralph.
I've been a long-time user of GitVersion,
which always worked well for me, but I wanted to try out something else for this.
Install prerequisites
Before gettings started, let's add Cake and MinVer as local tools in the repository.
> dotnet new tool-manifest
> dotnet tool install cake.tool
> dotnet tool install minver-cli
> dotnet tool restore
There should now be a new directory in your repository root called .config
which contains
the .NET tool manifest dotnet-tools.json
. By restoring the tools in the repository root,
we can run them without installing anything globally on our computer. Make sure that this
folder is not excluded in your .gitignore file.
# Restore tools
> dotnet tool restore
# Run cake
> dotnet cake
# Run minver
> dotnet minver
The GitHub Actions YAML
Now let us create our GitHub Actions YAML file that describes how to bootstrap our build process.
name: Publish
on:
push:
branches:
- main
jobs:
release:
name: Release
if: "!contains(github.event.head_commit.message, 'skip-ci')"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.200
- name: Build
shell: bash
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
DOTNET_CLI_TELEMETRY_OPTOUT: true
run: |
# Restore .NET tools
dotnet tool restore
# Get the version number
VERSION=$(dotnet minver -t v -v e -d preview)
# Run the build script
dotnet cake --target="Publish" --buildversion="$VERSION"
Nothing super complicated going on there. On every commit to main,
unless the commit message contains the text skip-ci
, we do the following
in bash on an Ubuntu image:
- Check out the code.
- Install the .NET Core SDK
- Restore the .NET tools
- Get the version number from MinVer and store it in a variable
- Pass the version number to the Cake build script as an argument and run the Publish target.
The build script
Our build script which we save as build.cake
in the repository root
looks like something like this:
#tool "nuget:?package=NuGet.CommandLine&version=5.5.1"
// Get the version argument and if none is provided, use 0.0.1.
// This is just for demonstrational purposes, you probably want to
// abort if publishing without providing a version number.
var semanticVersion = Argument("buildversion", "0.0.1");
var version = semanticVersion.Split(new char[] { '-' }).FirstOrDefault() ?? semanticVersion;
Task("Clean")
.Does(context =>
{
CleanDirectory("./.artifacts");
});
Task("Build")
.IsDependentOn("Clean")
.Does(context =>
{
DotNetCoreBuild("./src/MyProject.sln", new DotNetCoreBuildSettings {
Configuration = "Release",
NoIncremental = true,
MSBuildSettings = new DotNetCoreMSBuildSettings()
.WithProperty("Version", version)
.WithProperty("AssemblyVersion", version)
.WithProperty("FileVersion", version)
});
});
Task("Publish")
.IsDependentOn("Build")
.Does(context =>
{
// Make sure that there is an API key.
var apiKey = context.EnvironmentVariable("NUGET_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey)) {
throw new CakeException("No NuGet API key specified.");
}
// Pack all projects
context.DotNetCorePack($"./src/MyProject.sln", new DotNetCorePackSettings {
Configuration = "Release",
OutputDirectory = "./.artifacts",
NoBuild = true,
MSBuildSettings = new DotNetCoreMSBuildSettings()
.WithProperty("PackageVersion", semanticVersion)
});
// Publish all projects
foreach(var file in GetFiles("./.artifacts/*.nupkg"))
{
context.Information("Publishing {0}...", file.Path.GetFilename().FullPath);
context.NuGetPush(file, new NuGetPushSettings {
ApiKey = apiKey,
Source = "https://api.nuget.org/v3/index.json"
});
}
});
RunTarget(Argument("target", "Build"))
You would probably want to do a lot of other stuff there as well such as running tests and separating packing from publishing so you can get the packages without doing an actual publish, but this is just for demonstrational purposes. You could also call MinVer directly from your Cake script, or use it in other ways, but I like this approach since it's simple and easy to understand.
Why not do everything in the GitHub Action, you might think? Well, I like to decouple my builds from the build server I'm using, making it possible to run the same build on my machine.
Summary
In this blog post, I've described how to install local .NET tools whose definitions we store in the repository, creating GitHub YAML build definitions that is used to bootstrap our build process, and how to write and run a Cake build script which does the heavy lifting.