Creating a .NET Tool - Part 3: CI/CD
- 6 minutes read - 1198 wordsIn Part 2 of this series, we added the necessary code to package the console app as a .NET Tool that can be published to NuGet. In this post, we’re going all in on the DevOps, including pull request validation, publishing a CI package and publishing a Release package.
Note: I’ve gone ahead and added some tests so our CI/CD has something to test, so the main program looks slightly different compared to the first two posts, but hopefully not too much!
Versioning
Before setting up the CI/CD pipelines, let’s talk about versioning. Firstly, we want to use SemVer2.0 whereby:
- A release package version would look like
{major}.{minor}.{patch}
, e.g.1.0.0
, and be generated by creating a Release on GitHub - A pre-release package would look like
{major}.{minor}.{patch}-{pre-type}.{pre-num}
, e.g.1.0.0-alpha.1
, and be generated by creating a Release on GitHub - A ci-build package would look like
{major}.{minor}.{patch}-{pre-type}.{pre-num}.{git-commit}
, e.g.1.0.0-alpha.1.g8487asd8f
, and be generated by pushing to master
Luckily, there is a great OSS library that handles this quite nicely for us. It’s called Nerdbank.GitVersioning.
version.json
The first step is to set up the version.json
file that specifies your versioning strategy, like we’ve outlined above. Ours will look like:
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "1.0.0-alpha.1",
"gitCommitIdShortAutoMinimum": 7,
"nugetPackageVersion": {
"semVer": 2
},
"publicReleaseRefSpec": [
"^refs/tags/v\\d+\\.\\d+"
]
}
The $schema
tag is just for intellisense in IDE’s. We’re using gitCommitIdShortAutoMinimum
as 7
since that’s what GitHub’s short commit ID uses (this will be added to CI builds). We’re setting nugetPackageVersion
to use SemVer2.0 and finally adding any tags like vN.N
to publicReleaseRefSpec
so that the commit ID will not be added.
Nerdbank.GitVersioning
We also have to add a package reference to Nerdbank.GitVersioning which will handle the versioning during builds:
$ cd src/jsonv
$ dotnet add package Nerdbank.GitVersioning
Great, now we can start setting up CI/CD.
CI/CD
Given the code is on GitHub, it makes sense to use GitHub Actions for CI/CD. If you haven’t heard of GitHub Actions yet, go check it out. They’re not only for CI/CD but can really do anything you can think of, such as automatically labelling PR’s, mark issues and PR’s as stale and you can make you’re own (as we are doing here).
Branches
We know we want to have pull request validation, CI builds from master
and CD builds from vN.N
tags. We can set this up in our CI/CD action script with:
on:
pull_request:
branches:
- master # CI (pr validation)
push:
branches:
- master # CI (ci package)
tags:
- v* # CD (release package)
.NET environment
To build a .NET app we need to set up a .NET environment. The official actions/setup-dotnet can be used here:
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '3.1.201'
source-url: https://nuget.pkg.github.com/marcusturewicz/index.json
env:
NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
Here, we’re setting up .NET SDK 3.1.201 on Ubuntu 18.04 and setting the NuGet source-url
and NUGET_AUTH_TOKEN
for CI builds to be published to my GitHub Package Repository.
Continuous Integration
Now we’re in a position to do continuous integration. Meaning, we want to build, test, pack and publish (only on push) our package:
- run: dotnet build src/jsonv -c Release
- run: dotnet test test/jsonv.Tests -c Release
- run: dotnet pack src/jsonv -c Release
- run: dotnet nuget push src/jsonv/bin/Release/*.nupkg
if: github.event_name == 'push' && startswith(github.ref, 'refs/heads')
Notice the if
statement attached to the last run
statement which allows it to only run on a push to a branch; we don’t want to create packages for every pull request, only once the commit has made it’s way into master
.
Continuous Deployment
Now we have packages being published to our CI package feed from master
, we can move on to publishing packages from a release tag
:
- run: dotnet nuget push src/jsonv/bin/Release/*.nupkg -k ${{secrets.NUGET_ORG_API_KEY}} -s https://api.nuget.org/v3/index.json
if: github.event_name == 'push' && startswith(github.ref, 'refs/tags')
Here, we are pushing the package to nuget.org when a tag is created, using an API Key that I generated in my nuget.org account and added to my GitHub repo secrets. Again, notice the if statement which is controlling when this command runs.
Finally, the full action script is:
name: CI/CD
on:
pull_request:
branches:
- master # CI (pr validation)
push:
branches:
- master # CI (ci package)
tags:
- v\\d+\\.\\d+ # CD (release package)
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '3.1.201'
source-url: https://nuget.pkg.github.com/marcusturewicz/index.json
env:
NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
- run: dotnet build src/jsonv -c Release
- run: dotnet test test/jsonv.Tests -c Release
- run: dotnet pack src/jsonv -c Release
- run: dotnet nuget push src/jsonv/bin/Release/*.nupkg
if: github.event_name == 'push' && startswith(github.ref, 'refs/heads')
- run: dotnet nuget push src/jsonv/bin/Release/*.nupkg -k ${{secrets.NUGET_ORG_API_KEY}} -s https://api.nuget.org/v3/index.json
if: github.event_name == 'push' && startswith(github.ref, 'refs/tags')
For doing PR validation, CI builds and CD builds, I think it’s quite concise!
Seeing it in action
Let’s do some DevOps to see this all working together.
Pull request build
I’ve added the changes we made above to a new branch and created a pull request to merge into master. We can see that the pull request validation was triggered and passed successfully.
Let’s check out the build logs to see if everything worked correctly:
Great, we can see that build, test and pack were triggered but packages were not deployed to any feeds.
CI build
Now that the pull request has been merged to master, let’s see how our CI build went:
Great, we can see that everything built correctly and a CI build package was published to our internal feed correctly. Let’s check everything worked correctly with the package:
Great, the package was published correctly and has the version number that we expect, including the 7 character short git ID. This means for every commit to master we will have a package that we can install and test.
CD build
The final step is to publish a “Release” version of our package to nuget.org. Firstly, you’ll need to setup an account on NuGet and then create an API key:
Then, you copy the value and create a GitHub Secret in your repository using that value:
Now our action script will be able to read that value securely and pass it to a nuget publish command.
Now we are ready to create a GitHub Release and associated Tag:
Our CD build is then triggered due to the tag being created:
We can see that our package was built and deployed to NuGet:
Some things to note:
- NuGet recognises the package a pre-release due to the versioning
- SourceLink has linked the source repository
- The metadata looks good; image, description, authours and tags
Summary
In this post, we set up CI/CD such that each pull request to master is validated, CI packages published to GitHub Package Repository for each push to master and Release packages are published to nuget.org for each release created. We utilised Nerdbank.GitVersioning
to handle versioning, and GitHub Actions for our CI/CD pipeline. Finally, the tool is now available on nuget.org and the repository is ready for contributions! I hope this series has been helpful in how to easily create an OSS .NET Tool with proper DevOps practices around it.