December 30, 2024

Autoincrement project version with Gitlab CI – easy and 100% working

Hello guys and thanks for visiting my website! If you like what I’m writing here, you can become a supporter of my BuyMeACoffee page. If you have any comments, feel free to reach to me via email or any of the channels below. Feedback is much appreciated.

Having this said, let’s jump to some action! Today, I would like to share with you how I deal with automatic versioning in my Gitlab (private) projects. Proper versioning of your app is essential in the process of building, testing and releasing to production. While you can manually track major version increments (like 1.2.1 to 2.0.0), it’s very tedious to manage the build numbers. It can lead to dubious scenarios where you don’t really know what feature belongs to which version and so forth. For bigger projects, matters can be even worse…

A NodeJS app scenario

Things are clearer when put into practice. Let’s assume we have a NodeJS app with the version stored inside the package.json file.

{
  "name": "autoincrement",
  "version": "1.0.0",
  "description": "Auto increment project version with Gitlab CI",
  "main": "index.js",
  ...
}

We also have a simple index.js file that outputs the version set in the project:

console.log(`Our version is ${process.env.npm_package_version}!`)

If we run it with npm start it will show the following result:

$ npm start
Our version is 1.0.0!

As you can see, our app version is set to 1.0.0. If we’d like to work on a new release, we need to update this version by hand in order to increment it to let’s say 1.0.1. What happens if we need to test it on a test environment prior to publishing it to production and we need to merge changes from more developers? It would be menial to update this version on each commit to the develop branch manually. So the idea is to have this version increment done automatically for us and more than that, it should be persisted to the code repository.

IMPORTANT Please note that in order for the version to be printed correctly, the app must be started via the npm utility, otherwise the version will be undefined!

Let’s write an increment script in … Powershell

For this one I don’t insist that it should be done in Powershell necessarily, to me it was just at hand. You can use it as it is or you can write your own in Bash or whatever 😀

Here you go, a script that takes either a file as parameter or can get the version from piped input:

#!/usr/bin/pwsh

param(
    [parameter(ValueFromPipeline)]$input,
    [parameter(Position=0)] [String] $file
)

if (($file.length -le 0) -and ($input.length -le 0)) {
	Write-Error "You need to supply a file!"

	exit 123;
}

$content = $file.length -gt 0 ? (Get-Content $file | Select -First 1) : $input[0].ToString()
$fileVersion = $content.Split('.')
$fileVersion[-1] = [int]$fileVersion[-1] + 1
$fileVersion -join "."

The inner workings of this script are straightforward: it takes the input version as string, splits by “.”, converts the last element into a number, adds 1, joins back the updated version and prints it. You can get the version string required by this script from any file that you’d like be it a simple version.txt, an XML file or a JSON file like in our case. We now need a script that increments the version string from package.json. I got you covered as well:

#!/usr/bin/pwsh

if ($args.length -le 0) {
	Write-Error "You need to supply a file!"

	exit 123;
}

$file = $args[0]
$packageJson = $(Get-Content $file | ConvertFrom-Json)
$packageJson.version = $packageJson.version | & "$PSScriptRoot/increment-version.ps1"
$packageJson | ConvertTo-Json > $file

Again, this script is very straightforward: we pass the required package.json file, we use the ConvertFrom-Json Powershell function to access the JSON programatically. Then we call the increment version script. The incremented value is then put back into the JSON and wrote to disk by using the ConvertTo-Json utility.

The CI pipeline in Gitlab

It’s now high time to proceed on integrating it with the Gitlab pipeline. For sure you don’t want to run those scripts by hand, do you? 😎

If your answer is YES, let’s continue. As you probably know, Gitlab uses the .gitlab-ci.yml file to define pipeline stages and steps. I will assume that our sample app has 3 stages, each one with one step:

  1. Build – we will just mimic some build
  2. Increment version – increment the version by calling the scripts
  3. Commit and push back to the repository – yeah, this is a tip-top goodie, very tricky, but I got you covered again 😉

Now let’s see how this looks like in the .gitlab-ci.yml:

stages:
  - build
  - versioning
  - commit

build-app:
  stage: build
  image: node:14.18
  only: 
    - main
    - /^dev-.*$/
  script:
    - echo "Building ..."
    - sleep 5
    - echo "Done fake build"

increment-versions:
  stage: versioning
  image: mcr.microsoft.com/powershell
  variables:
  only:
    - main
  artifacts:
    paths:
      - package.json
      - tag.txt
  script:
    # Store versions in env variables
    - ./get-version-package.ps1 ./package.json > tag.txt
    - ./increment-version-package.ps1 ./package.json

commit-versions:
  stage: commit
  image: bitnami/git
  variables:
    USER: admin
    MAIL: admin@smth.com
    # Make sure you replace this with your actual repo
    ORIGIN: git@gitlab.com:afivan/node-autoincrement.git
  only:
    - main
  dependencies:
    - increment-versions
  before_script:
   - 'which ssh-agent || ( apt-get update -qy && apt-get install openssh-client -qqy )'
   - eval `ssh-agent -s`
   - cat $SSH_PRIVATE_KEY | tr -d '\r' | ssh-add - > /dev/null
   - mkdir -p ~/.ssh
   - chmod 700 ~/.ssh
   - echo $SSH_PUBLIC_KEY >> ~/.ssh/id_rsa.pub
   - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
   - export VERSION=`cat tag.txt`
  script:
    # Git commit and push (without triggering pipeline)
    - git config --global user.email $MAIL
    - git config --global user.name $USER
    - git tag -a $VERSION -m "Version $VERSION"
    - git commit -a -m "AUTO version increase [skip ci]"
    - git remote set-url --push origin $ORIGIN
    - git push --tags origin HEAD:$CI_COMMIT_REF_NAME

The build stage should be very straightforward. Here you’ll need to proceed with the steps required for your project. For the increment-versions stage, I basically defined 2 artifacts that are passed to the commit stage and incremented the version after I save the current version to the tag.txt file. I use tag.txt to create a version tag into git to track versions. You can also use the DotEnv feature of Gitlab, I didn’t go into it too much.

The commit stage

The cherry pie of this is the last stage, the commit. This is trickier to understand and to make it work we need to define a SSH key to be used to commit back to the repository. You need to go to the Repository => Settings => Deploy Keys and use the instructions to define one. Make sure you tick the “Grant write permissions to this key” box. Paste only the public key in the box at this stage.

Cool, now if you generated this key correctly, you should have 2 files one with the public key the other with the private key. We need to store these 2 in the project variables so that they get picked by the running job.

IMPORTANT: Make sure you name them correctly, ensure they are protected and that the SSH_PRIVATE_KEY is set as File type.

Testing the whole thing

Now let’s test the pipeline and see how it works. The pipeline should be triggered at each commit. Now it’s up to you to choose the branch where you increment the version and push back. Usually in my projects I use the dev branch but for this demo I left the push to happen on master. You should be able to see in CI/CD the running pipeline:

If everything is fine, you should see at the end a new commit with a skipped pipeline that contains the incremented version:

And voila! The magic happened, every commit to the respective branch will autoincrement the project version! The demo repository can be found here

Wrapping up

In the end, no technical challenge is left without a solution. With Gitlab, things are very easy to customize to your specific needs. That’s why I think it’s a great tool that is way better than let’s say Azure DevOps. So feel free to experiment with it and let me know how it works for you!

Thanks for reading, I hope you found this article useful and interesting. If you have any suggestions don’t hesitate to contact me. If you found my content useful please consider a small donation. Any support is greatly appreciated! Cheers  😉

afivan

Enthusiast adventurer, software developer with a high sense of creativity, discipline and achievement. I like to travel, I like music and outdoor sports. Because I have a broken ligament, I prefer safer activities like running or biking. In a couple of years, my ambition is to become a good technical lead with entrepreneurial mindset. From a personal point of view, I’d like to establish my own family, so I’ll have lots of things to do, there’s never time to get bored 😂

View all posts by afivan →