How to properly cache the node_modules directory in GitlabCI?

22 February 2023 | Eric

Below is an example of a proper strategy to cache the node_modules folder in GitlabCI.

It is vastly inspired by the Gitlab documentation.

It uses the package-lock.json as the cache key, which allows us to rebuild the cache whenever the file is modified. Alternatively, you could use the package.json as the cache key, depending on your specific needs and workflow.

The code below is self-explanatory and contains helpful comments. However, it is important to note it creates the node_modules folder including the devDependencies! To avoid this, you can either add the --production parameter to the npm ci command or set your NODE_ENV to production.

# Global settings that is applied to every job
cache:
  # Create a key using the package-lock
  - key: &global_cache_node_mods
      files:
        - package-lock.json
    paths:
      - node_modules/
    # Prevent subsequent jobs from modifying cache
    policy: pull

install:
  # .pre stage mean it's executed before everything else
  stage: .pre
  cache:
    # Mimic &global_cache_node_mods config but override policy
    # to allow this job to update the cache at the end of the job
    # and only update if it was a successful job
    - key:
        files:
          - package-lock.json
      paths:
        - node_modules/
      when: on_success
      policy: pull-push # update the cache

    # store npm cache for all branches (stores download pkg.tar.gz's)
    # will not be necessary for any other job because we don't install at every step
    - key: ${CI_JOB_NAME}
      # must be inside $CI_PROJECT_DIR for gitlab-runner caching (#3)
      paths:
        - .npm/
      when: on_success
      policy: pull-push
  script:
    # Change the cache directory so it's in $CI_PROJECT_DIR instead of ~/.npm
    # Make sure you use the --production parameter or set your NODE_ENV to
    # `production` if you don't want the devDependencies to be installed
    - npm ci --cache .npm --prefer-offline
  only:
    changes:
      - package-lock.json
    refs:
      - merge_requests
      - main
      - develop

# Linting our code
lint:
  stage: test
  script:
    - if ! [ -d node_modules ]; then npm ci --cache .npm --prefer-offline; fi
    - npm run lint
  only:
    changes:
      - package-lock.json
      - src/**/*
      - test/**/*
    refs:
      - merge_requests
      - main
      - develop

# Example of running test with coverage using Vitest
vitest:
  stage: test
  script:
    - if ! [ -d node_modules ]; then npm ci --cache .npm --prefer-offline; fi
    - npm run coverage
  artifacts:
    name: "coverage-$CI_COMMIT_SHA"
    paths:
      - coverage
    expire_in: 1h
  only:
    changes:
      - package-lock.json
      - src/**/*
      - test/**/*
    refs:
      - merge_requests
      - main
      - develop