Automate versioning and changelog with release-it on GitLab CI/CD

It’s said that you should automate all the things and one of the things could be versioning your software. Incrementing the version number in your e.g. package.json is easy but it’s easier when you bundle it to your continuous integration and continuous deployment process. There are different tools you can use to achieve your needs and in this article we are using release-it. Other options are for example standard-version and semantic-release.

🚀 Automate versioning and package publishing

Using release-it with CI/CD pipeline

Release It is a generic CLI tool to automate versioning and package publishing related tasks. It’s installation requires npm but package.json is not needed. With it you can i.a. bump version (in e.g. package.json), create git commit, tag and push, create release at GitHub or GitLab, generate changelog and make a release from any CI/CD environment.

Here is an example setup how to use release-it on Node.js project with Gitlab CI/CD.

Install and configure release-it

Install release-it with npm init release-it which ask you questions or manually with npm install --save-dev release-it .

For example the package.json can look the following where commit message has been customized to have “v” before version number and npm publish is disabled (although private: true should be enough for that). You could add [skip ci] to “commitMessage” for i.a. GitLab CI/CD to skip running pipeline on release commit or use Git Push option ci.skip.

package.json
{
  "name": “example-frontend",
  "version": "0.1.2",
  "private": true,
  "scripts": {
    ...
    "release": "release-it"
  },
  "dependencies": {
    …
  },
  "devDependencies": {
    ...
    "release-it": "^12.4.3”
},
"release-it": {
    "git": {
      "tagName": "v${version}",
      "requireCleanWorkingDir": false,
      "requireUpstream": false,
      "commitMessage": "Release v%s"
    },
    "npm": {
      "publish": false
    }
  }
}

Now you can run npm run release from the command line:

npm run release
npm run release -- patch --ci

In the latter command things are run without prompts (–ci) and patch increases the 0.0.x number.

Using release-it with GitLab CI/CD

Now it’s time to combine release-it with GitLab CI/CD. Adding release-it stage is quite straigthforward but you need to do couple of things. First in order to push the release commit and tag back to the remote, we need the CI/CD environment to be authenticated with the original host and we use SSH and public key for that. You could also use private token with HTTPS.

  1. Create SSH keys as we are using the Docker executorssh-keygen -t ed25519.
  2. Create a new SSH_PRIVATE_KEY variable in “project > repository > CI / CD Settings” where and paste the content of your private key that you created to the Value field.
  3. In your “project > repository > Repository” add new deploy key where Title is something describing and Key is the content of your public key that you created.
  4. Tap “Write access allowed”.

Now you’re ready for git activity for your repository in CI/CD pipeline. Your .gitlab-ci.yaml release stage could look following.

Update 2021-07-30: release-it has updated documentation for running it on CI and doing releases which differs a bit from the below.

image: docker:19.03.1

stages:
  - release

Release:
  stage: release
  image: node:12-alpine
  only:
    - master
  before_script:
    - apk add --update openssh-client git
    # Using Deploy keys and ssh for pushing to git
    # Run ssh-agent (inside the build environment)
    - eval $(ssh-agent -s)
    # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    # Create the SSH directory and give it the right permissions
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    # Don't verify Host key
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    - git config user.email "gitlab-runner@your-domain.com"
    - git config user.name "Gitlab Runner"
  script:
    # See https://gist.github.com/serdroid/7bd7e171681aa17109e3f350abe97817
    # Set remote push URL
    # We need to extract the ssh/git URL as the runner uses a tokenized URL
    # Replace start of the string up to '@'  with git@' and append a ':' before first '/'
    - export CI_PUSH_REPO=$(echo "$CI_REPOSITORY_URL" | sed -e "s|.*@\(.*\)|git@\1|" -e "s|/|:/|" )
    - git remote set-url --push origin "ssh://${CI_PUSH_REPO}"
    # runner runs on a detached HEAD, checkout current branch for editing
    - git checkout $CI_COMMIT_REF_NAME
    - git pull origin $CI_COMMIT_REF_NAME
    # Run release-it to bump version and tag
    - npm ci
    - npm run release -- patch --ci --verbose

We are running release-it here with patch increment. If you want to skip CI pipeline on release-it commit you can either use the ci.skip Git Push option package.json git.pushArgs which tells GitLab CI/CD to not create a CI pipeline for the latest push. This way we don’t need to add [skip ci] to commit message.

And now you’re ready to run the pipeline with release stage and enjoy of automated patch updates to your application’s version number. And you also get GitLab Releases if you want.

Setting up the script step was not so clear but fortunately people in the Internet had done it earlier and Google found a working gist and comment on GitLab issue. Interacting with git in the GitLab CI/CD could be easier and there are some feature requests for that like allowing runners to push via their CI token.

Customizing when pipelines are run

There are some more options for GitLab CI/CD pipelines if you want to run pipelines after you’ve tagged your version. Here’s snippet of running “release” stage on commits to master branch and skipping it if commit message is for release.

Release:
  stage: release
  image: node:12-alpine
  only:
    refs:
      - master
    variables:
      # Run only on master and commit message doesn't start with "Release v"
      - $CI_COMMIT_MESSAGE !~ /^Release v.*/
  before_script:
    ...
  script:
    ...

Now we can build a new container for the deployment of our application after it has been tagged and version bumped. Also we are reading the package.json version for tagging the image.

variables:
  PACKAGE_VERSION: $(cat package.json | grep version | head -1 | awk -F= "{ print $2 }" | sed 's/[version:,\",]//g' | tr -d '[[:space:]]')

Build dev:
  before_script:
    - export VERSION=`eval $PACKAGE_VERSION`
  stage: build
  script:
    - >
      docker build
      --pull
      --tag your-docker-image:latest
      --tag your-docker-image:$VERSION.dev
      .
    - docker push your-docker-image:latest
    - docker push your-docker-image:$VERSION.dev
  only:
    refs:
      - master
    variables:
      # Run only on master and commit message starts with "Release v"
      - $CI_COMMIT_MESSAGE =~ /^Release v.*/

Using release-it on detached HEAD

In the previous example we made a checkout to current branch for editing as the runner runs on detached HEAD. You can use the detached HEAD as shown below but the downside is that you can’t create GitLab Releases from the pipeline as there’s no git tag for the release and thus it fails to “ERROR Response code 422 (Unprocessable Entity)” (see also issue #533).

Then the .gitlab-ci.yml is following:

...
script:
    - export CI_PUSH_REPO=$(echo "$CI_REPOSITORY_URL" | sed -e "s|.*@\(.*\)|git@\1|" -e "s|/|:/|" )
    - git remote set-url --push origin "ssh://${CI_PUSH_REPO}"
    # gitlab-runner runs on a detached HEAD, create a temporary local branch for editing
    - git checkout -B ci_processing
    # Run release-it to bump version and tag
    - npm ci
    - DEBUG=release-it:* npm run release -- patch --ci --verbose --no-git.push
    # Push changes to originating branch
    # Always return true so that the build does not fail if there are no changes
    - git push --follow-tags origin ci_processing:${CI_COMMIT_REF_NAME} || true

Using release-it and doing GitLab releases

Using release-it to tag and release versions on GitLab CI is quite straightforward when using the detached HEAD approach but if you want to do GitLab release for the version you need to set the release step separately and have a changelog generation set up in package.json. Like below:

package.json

"release-it": {
    ...
    "hooks": {
      "before:init": "git fetch --prune --prune-tags origin",
      "after:bump": "./generate-release-notes.sh"
    },
    "gitlab": {
      "release": false,
      "releaseName": "v${version}",
      "releaseNotes": "./generate-release-notes.sh ${latestVersion} v${version}"
    }
    ...
  }

generate-release-notes.sh

#!/usr/bin/env sh
node_modules/conventional-changelog-cli/cli.js -p angular -r 1

.gitlab-ci.yml

...
script:
    - git checkout -B ci_processing
    # Install packages
    - npm ci --legacy-peer-deps
    # Run release-it to bump version and tag
    - DEBUG=release-it:* npm run release -- --ci --preRelease --verbose --no-git.push --disable-metrics
    # Push changes to originating branch
    # Always return true so that the build does not fail if there are no changes
    - git push --follow-tags origin ci_processing:${CI_COMMIT_REF_NAME} || true
    # Do GitLab Release
    - npm run release -- --ci --no-increment --no-git --gitlab.release


Posted

in

,

by

Comments

4 responses to “Automate versioning and changelog with release-it on GitLab CI/CD”

  1. Lucas Avatar
    Lucas

    First of all thanks for the awesome article!

    I’ve tried your code, I just changed some sed commands regarding the git host.
    In Gitlab CI as you mentioned the runner runs on a detached HEAD, and i suspect this is the reason for the error I am getting. The current error I get is:

    “`
    $ git pull origin $CI_COMMIT_REF_NAME
    From https:///
    * branch master -> FETCH_HEAD
    Auto-merging package.json
    CONFLICT (content): Merge conflict in package.json
    Automatic merge failed; fix conflicts and then commit the result.
    “`

    That occurs because checking out the origin master returns:

    “`
    $ git checkout $CI_COMMIT_REF_NAME
    Previous HEAD position was 63d6d5a
    Switched to branch ‘master’
    Your branch and ‘origin/master’ have diverged,
    and have 1 and 12 different commits each, respectively.
    (use “git pull” to merge the remote branch into yours)
    “`

    I would really love to automate release management with the release-it CLI but I can’t seem to find a solution to this. Have you – by any chance – come accross this or an similar error or can you comment your thoughts on this? Any help is greatly appreciated!

    1. Marko Avatar

      Are you doing the release-it on a separate branch instead of a master branch?

      The examples above are running the release-it only on master where there isn’t a problem of merge conflicts which are impossible to merge in CI pipeline automatically.

      1. Lucas Avatar
        Lucas

        Hi there,

        Thanks for the quick reply!
        No I ran the release-it jobs on master.
        These merge conflicts happened (and are still happening) on the master branch after resetting and cleaning the repo.

        1. Marko Avatar

          Yep, I have the same problem when following the release process how release-it documents it on GitLab CI. Always conflicts. The only way I got it working was using the temporary branch. Also when using GitLab Releases there’s some more tasks to do to get release notes and bumping version by conventional commits. I’ve added a section about that.

Leave a Reply

Your email address will not be published. Required fields are marked *