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
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.
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
MAJORversion number, depending on the type of change
- finishing release containing one or multiple hotfixes will increment
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.
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.
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
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.
Next, we doefine that our pipeline will consist of two stages:
push. Sages are executed sequentially, in the order in which they are specified.
stages: - build - push
build stage, there is a single
build job that - you guessed it - builds our project. This job is executed on each push to
build: stage: build script: - cd src - dotnet build
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.
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
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.