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 or MAJOR 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.

protected_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.

protected_key

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.