A tutorial on how to use Gitlab CI, in combination with GitFlow branching model and semantic versioning scheme for continuous delivery of Nuget packages.
First things first, know your terms
Semantic versioning is a widely implemented versioning scheme that defines version numbers be formed as MAJOR.MINOR.PATCH
. Following this scheme, you would increment MAJOR
version when you make incompatible changes, MINOR
version when you add functionality in a backwards-compatible manner, and PATCH
version when you make backwards-compatible bug fixes.
You can read more about semantic versioning here.
GitFlow is a branching model very well suited for collaboration. It defines a single master
and a single development
branch, alongside with multiple feature
, release
and hotfix
branches. The idea is that the master
branch has production code, each new feature is developed in its dedicated feature
branch that is forked off a development
branch, and when it’s finished, merged back into development
. Once a single, or more features are ready to be relased, a release
branch gets created, and is merged into master
once the release is finished. At this point, release commit is also tagged with its name.
More on GitFlow branching model can be read here. And there’s also a particularly handy GitFlow cheat sheet available here.
GitLab offers powerful and simple to configure CI/CD pipeline services which we will be using in this tutorial to continuously push new versions of our packages to nuget.org.
Bits and pieces
In order for this to work, we need to set some standrds. Let us agree to be naming our release
branches like MAJOR.MINOR.PATCH
. This way, equally named tags will be created when releases are finished. And builds from these tags will be pushed to nuget as new package versions.
We will have to always make sure to release features and hotfixes sperately, so that each release gets the proper version number incremented:
- finishing release containing one or multiple features will increment
MINOR
orMAJOR
version number, depending on the type of change - finishing release containing one or multiple hotfixes will increment
PATCH
version number
We will setup GitLab CI so that whenever a new tag is pushed to the repository, it will trigger a job that generates new version of a package and pushes it to nuget.org.
Simple, innit?
Demo
For demonstration purposes, we’re going to setup continuous delivery for our SemanticNuGitFlow package that does absolutely nothing, but nonetheless has a proper great name. Repository for this demo is available at gitlab, naturally.
Gitflow
We’ll start by initializing support for GitFlow branching model in our repository.
git flow init
We’ll start new feature. This will create and switch to a new feature branch, probably named feature/amazing-feature
(if you accepted default naming conventions during init).
git flow feature start amazing-feature
We’ll implement some great functionality and commit it.
git add .
git commit -m "Amazing feature implemented."
Then we’ll want to finish the feature. This will merge feature branch into development, delete it and switch to development branch.
git flow feature finish
We’re happy with what we’ve got and now we wish to start the new release. This will create a new release branch off develop, named release/1.1.0
, and switch to it.
git flow release start 1.1.0
Here, we can eventually commit some last minute changes in preparation for release, and when it’s ready, simply finish it. This will merge release branch into master, tag the release with its name 1.1.0
and delete the release branch.
git flow release finish
All that is left for us to do is to push everything, both code and tags, to the remote repository. This will trigger a CI job that will publish a new package version 1.1.0
for us.
git checkout master
git push origin master --tags
GitLab CI
The only file we really care about in our demo SemanticNuGitFlow repository is .gitlab-ci.yml
. It is where GitLab CI is configured. It is really not that big and is rather simple, so let’s inspect it.
Right at the top, the very first line specifies base Docker image our pipeline will use to run. Since we’re building .NET Core nuget package, our CI pipeline will have to use the official dotnet Docker image. Makes sense.
image: microsoft/dotnet:latest
Next, we doefine that our pipeline will consist of two stages: build
and push
. Sages are executed sequentially, in the order in which they are specified.
stages:
- build
- push
In build
stage, there is a single build
job that - you guessed it - builds our project. This job is executed on each push to master
branch.
build:
stage: build
script:
- cd src
- dotnet build
Job push
that is defined to run in push
stage gets ecxecuted only when a new tag is pushed to the repository.
It creates a new nuget package, setting its version to the tag which triggered the job and which is available through CI_COMMIT_TAG
environment variable of the GitLab runner. Next, it pushes this package to nuget.org.
push:
stage: push
only:
- tags
script:
- cd src
- dotnet pack /p:Version=$CI_COMMIT_TAG -o ./
- dotnet nuget push $project.$CI_COMMIT_TAG.nupkg --api-key $NUGET_KEY -s https://www.nuget.org/api/v2/package
artifacts:
paths:
- src/$project.$CI_COMMIT_TAG.nupkg
In order to push packages to nuget, one must obtain an API key for nuget CLI. We’ve stored this key in a NUGET_KEY
environment variable for our project. This can be done through Gitlab web interface, by going to your project -> Settings -> CI/CD -> Environment variables. Note that, since this variable stores a secret key, you should set it to be a proteccted variable.
In order to use protected variable on a tag-triggered job, you should also set all tags to be protected, by going to your project -> Settings -> Repository -> Protected Tags, like shown in the image below.
If you want to know more about Gitlab CI variables, see here. For some extra reading about protected tags, proceed to here.
That’s about it! Here’s the complete CI config file. Haven’t I told you it was simple?
image: microsoft/dotnet:latest
stages:
- build
- push
variables:
project: "SemanticNuGitFlow"
build:
stage: build
script:
- cd src
- dotnet build
push:
stage: push
only:
- tags
script:
- cd src
- dotnet pack /p:Version=$CI_COMMIT_TAG -o ./
- dotnet nuget push $project.$CI_COMMIT_TAG.nupkg --api-key $NUGET_KEY -s https://www.nuget.org/api/v2/package
artifacts:
paths:
- src/$project.$CI_COMMIT_TAG.nupkg
That’s it
I hope you will agree that using Gitlab CI in combination with GitFlow methodolgy and semantic versioning scheme has proven a simple and useful mechanism for continuously delivering your nuget packages. If that’s not the case, or you think there’s somethinng wrong with this approach, do not hesitate to drop a comment below.