Gitlab CI/CD for npm packages

A couple of weeks ago the IT team in my company talked about having repositories for the packages we make for our PHP applications so we can switch to a more natural use of composer. We left the meeting with ideas but not with a concrete solution nor a promise to research this topic.
Few days ago I needed to make a javascript package, after creating a repository on our gitlab I noticed an option for Packages & Registries. As it blew my mind that such an option exists I decided to research it a little and use it for this javascript package if possible.
Here is what I learned in the process.

Options

Gitlab offers a few registries you can work with: Composer, Conan, Maven, NPM, NuGet, PyPi. I have only tried out the NPM registry, but others should also be easy to work with.

Publishing an NPM package to registry

This was actually my first time making an NPM package. So I would like to recommend this post Step by Step building and publishing an NPM typescript package to the first timers like me. It was very easy to understand and no steps were missed.

First of all in your package.json you should scope your project cause Gitlab requires packages to be scoped.
For example:

{
  "name": "@scope/example-package-name",
  "version": "1.0.0"
}

After we have this setup, if we use a .npmrc file or npm config set registry we can tell npm where we want it to publish our package. Looks something like this:

//gitlab.example.com/api/v4/projects/${PROJECT_ID}/packages/npm/:_authToken=${GITLAB_DEPLOY_TOKEN}

If the repository is set to internal or private you need to use a Gitlab deploy token. On how to get one, you can read at Deploy tokens documentation.

After running npm publish you should be able to see your package in the registry of your repository.

And you should be able to see a version 1.0.0 that says it was pushed manually.

CI/CD

To make our life and the life of our colleagues better we can make good use of the gitlabs CI/CD system here.
We can use .gitlab-ci.yml configuration that looks like this:

stages:
  - build
  - test
  - publish

build:
  stage: build
  only:
    - tags
  cache:
    key: build-cache
    paths:
      - node_modules/
      - lib/
      - .npmrc
    policy: push
  script:
    - echo "//gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}">.npmrc
    - docker run -v $(pwd):/app -v /home:/home -w="/app" -u="$(id -u):$(id -g)" -e HOME node:14 npm install
    - docker run -v $(pwd):/app -v /home:/home -w="/app" -u="$(id -u):$(id -g)" -e HOME node:14 npm run build
test:
  stage: test
  only:
    - tags
  cache:
    key: build-cache
    paths:
      - node_modules/
      - lib/
      - .npmrc
    policy: pull
  script:
    - docker run -v $(pwd):/app -v /home:/home -w="/app" -u="$(id -u):$(id -g)" -e HOME node:14 npm run test
lint:
  stage: test
  only:
    - tags
  cache:
    key: build-cache
    paths:
      - node_modules/
      - lib/
      - .npmrc
    policy: pull
  script:
    - docker run -v $(pwd):/app -v /home:/home -w="/app" -u="$(id -u):$(id -g)" -e HOME node:14 npm run lint

publish:
  stage: publish
  only:
    - tags
  cache:
    key: build-cache
    paths:
      - node_modules/
      - lib/
      - .npmrc
    policy: pull
  script:
    - docker run -v $(pwd):/app -v /home:/home -w="/app" -u="$(id -u):$(id -g)" -e HOME node:14 npm version --no-git-tag-version ${CI_COMMIT_TAG}
    - docker run -v $(pwd):/app -v /home:/home -w="/app" -u="$(id -u):$(id -g)" -e HOME node:14 npm publish

Notable points:

  • In build stage we make a .npmrc file that contains the path of the registry made by using the CI environment variables
  • All the stages run only on tags, a special way to tell the CI/CD system to only activate when you tag the code in your repository
  • We build a cache for node_modules, lib and .npmrc as such we limit the number of scripts we need to run after the build step
  • Only the build step makes the cache others only use it, it is defined by push/pull policy
  • In publish stage we use a npm version --no-git-tag-version ${CI_COMMIT_TAG} command. npm version is a noisy command that tags and commits code if it detects a directory being a git repository so that's why we use --no-git-tag-version here. As the stage was triggered by us tagging the code, we have the ${CI_COMMIT_TAG} environment variable available to use for package versioning. After that we just publish the package.

Note
I didn't have a gitlab runner that was setup to use docker normally nor did I have node and npm installed on the machine so I had to use docker run commands like shown. So... not the most elegant way of doing it.

The end result is this:
Pipeline

Now the developers don't have to run any scripts locally, just to commit to the repository and tag the code.

If you'd like to support me writing feel free to buy me a cup of coffee.

23