How to automate releases in Gitlab using Standard Version and NPM

This post shows how to set up automated, tagged, semantically-versioned releases, changelog generation, and CI testing using a Gitlab pipeline, Standard Version and Conventional Commits.

In order to make this work, we need a few files:

  • A gitlab-ci.yml pipeline file with a correct configuration
  • A package.json file for the sake of pulling standard-version and tracking the current version number.
  • [Optional] A verion_bump.js file to replace static version strings within the codebase (if required)

Challenges faced

  • Gitlab would get stuck in an infinite loop when a releases chore commit is pushed back to master and develop, triggering further builds and releases.
  • Replacing static strings in files was difficult to make work with standard-version. I eventually compromised by having the aforementioned version_bump.js file output a commit message.
  • Having standard-version commit additional files was also difficult to figure out.

Caveats

  • Gitlab caches branches locally and uses git fetch. Change this to git clone if you are struggling with getting changes back into Git due to errors on push.
  • Rerunning the same Gitlab job reuses the same workspace, including any stale commits leftover from before a --force-push. Nasty.

For these reasons (and to document the process for myself) I wrote this post in the hope that it will save you some time.

Step 1. Add a .gitlab-ci.yml file

The pipeline configuration involves a job for testing (executed on all branches) and a release job which is executed exclusively on the master branch.

gitlab-ci.yml

variables:
    CI_NAME: "gitlab"
    CI_EMAIL: "gitlab-ci@example.com"
stages:
  - test
  - release
test:
  stage: test
  except:
    variables:
      - $GITLAB_USER_LOGIN == $CI_NAME

  script:
    - echo $GITLAB_USER_LOGIN
    - echo $CI_USER
    - echo $CI_NAME
   

release:
  stage: release
  when: on_success

  except:
    variables:
      - $GITLAB_USER_LOGIN == $CI_NAME
      
  tags: 
    - npm
  only:
    - master
  image: tarampampam/node:alpine
  script:
    - npm install
    - git config --global user.email $CI_EMAIL
    - git config --global user.name $CI_NAME
    - git config receive.advertisePushOptions true
    - git checkout -B "$CI_COMMIT_REF_NAME" "$CI_COMMIT_SHA"
    - npm run release
    - git push http://${CI_USER}:${CI_ACCESS_TOKEN}@REPO_URL --follow-tags master:master
    - git checkout develop
    - git merge master
    - git push http://${CI_USER}:${CI_ACCESS_TOKEN}@REPO_URL --follow-tags develop:develop

Replace REPO_URL with the URL to your repository. I had to include the port number (80 in my case) as well.

Step 2: Add Project level CI variables in Gitlab

Add CI_ACCESS_TOKEN and CI_USER variables with an access token/user that has access to the project.

CI Variables

Step 3: Add package.json

The following package.json file contains the minimum configuration required to make standard-version work. (The replace-in-file dependency is only required if you need to replace the version string in some files.)

package.json

{
  "name": "jekyll-browser-startpage",
  "version": "0.1.0",
  "description": "A browser startpage.",
  "main": "index.js",
  "directories": {
    "test": "tests"
  },
  "scripts": {
    "release": "standard-version -a"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/danobot/jekyll-browser-start"
  },
  "author": "Daniel Mason",
  "license": "MIT",
  "devDependencies": {
    "semantic-release": "^15.13.3",
    "standard-version": "^4.4.0",
    "replace-in-file": "^3.4.3"
  },
  "standard-version": {
    "scripts": {
      "precommit": "node version_bump.js && git add startpage/_includes"
    }
  }
}

Notes:

  • In my experience, the precommit node script must reference a Javascript file in the root of the project directory.
  • If you have issues with node not running your script or Gitlab not committing the changed file, then copy the directory layout exactly. There are weird issues where subfolders cannot be found.

Step 4: Add a custom script to substitute version strings

This step is optional. if there are files where the version string is referenced (such as a HTML partial), then use the version+bump.js file below to regex replace the version string. The output of this file is used by standard-version as the commit message.

version_bump.js

var v = require('./package.json').version
console.log(v)
const replace = require('replace-in-file');
const regex = new RegExp(/.*/, 'i');
const options = {
    files: 'startpage/_includes/version.html',
    from: regex,
    to: "v" + v,
};

var changes = replace.sync(options)

console.log("chore(release): " + v)

That’s it

You should now have a working pipeline that will run the test job on non-master branches and will prepare a release version with auto-generated changelog when Merge Requests are merged onto master. The image below shows the pipeline. CI Merge

Change Log (Markdown rendered)

All notable changes to this project will be documented in this file. See standard-version for commit guidelines.

3.1.0 (2019-02-26)

Features

  • blocked mode: add timeout to blocked mode such that the controller takes over after some time. (9160879)

3.0.1 (2019-02-26)

Bug Fixes

  • tracker: update component name and location (91e4950)

3.0.0 (2019-02-26)

Chores

  • rename component, migrate to new directory/file format (889d5cd)

BREAKING CHANGES

  • component has been renamed to entity_controller and migrated to the new file/directory format. To update your configuration, hard-replace lightingsm with entity_controller in your configuration files and Lovelace config. The directory/file format change may require you go into your custom_components folder and manually remove the lightingsm.py file and create the new directory structure.