Skip to content

GitLab CI template for Node.js

This project implements a GitLab CI/CD template to build, test and analyse your JavaScript/TypeScript/Node.js projects.

More precisely, it can be used by all projects based on npm, yarn or pnpm package managers.

Usage

This template can be used both as a CI/CD component or using the legacy include:project syntax.

Use as a CI/CD component

Add the following to your .gitlab-ci.yml:

include:
  # 1: include the component
  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node@4.1.0
    # 2: set/override component inputs
    inputs:
      image: "registry.hub.docker.com/library/node:20" # ⚠ this is only an example
      lint-enabled: "true"

Use as a CI/CD template (legacy)

Add the following to your .gitlab-ci.yml:

include:
  # 1: include the template
  - project: "to-be-continuous/node"
    ref: "4.1.0"
    file: "/templates/gitlab-ci-node.yml"

variables:
  # 2: set/override template variables
  NODE_IMAGE: "registry.hub.docker.com/library/node:20" # ⚠ this is only an example
  NODE_LINT_ENABLED: "true"

Global configuration

The Node.js template uses some global configuration used throughout all jobs.

Input / Variable Description Default value
image / NODE_IMAGE The Docker image used to run Node.js
⚠ set the version required by your project
registry.hub.docker.com/library/node:lts-alpine
manager / NODE_MANAGER The package manager used by your project (one of npm, yarn or pnpm)
If undefined, automatic detection
none (auto)
config-registry / NODE_CONFIG_REGISTRY Main npm registry to use none
config-scoped-registries / NODE_CONFIG_SCOPED_REGISTRIES Space separated list of npm scoped registries (formatted as @somescope:https://some.npm.registry/some/repo @anotherscope:https://another.npm.registry/another/repo) none
project-dir / NODE_PROJECT_DIR Node project root directory .
source-dir / NODE_SOURCE_DIR Sources directory src
install-extra-opts / NODE_INSTALL_EXTRA_OPTS Extra options to install project dependencies (either npm ci, yarn install or pnpm install) none

Using scoped registries

Scoped registries allow to pull and publish packages using multiple registries.

Examples:

  • npm install foo installs foo package from https://www.npmjs.com/ by default,
  • npm install @angular/core installs @angular/core package from https://www.npmjs.com/ if no npm registry associated to scope @angular is declared,
  • npm install @acme-corp/bar installs @acme-corp/bar package from https://registry.acme.corp/npm if this registry url is associated to scope @acme-corp.

First of all, be aware that the Node.js template automatically configures the GitLab's project-level npm packages registry associated to a scope corresponding to the root of the project (ex: project https://gitlab.example.com/my-org/engineering-group/team-amazing/analytics will have GitLab's project-level npm packages registry scope @my-org). Therefore, GitLab's project-level npm packages registry can freely be used both to install packages (with the right scope) or even to publish your own packages.

You may configure additional scoped registries with the $NODE_CONFIG_SCOPED_REGISTRIES variable. The value is expected as a (whitespace-separated) list of @registry-scope:registry-url.

The Node.js template also supports authentication for each, simply by defining the appropriate variable (as project or group secret variables) depending on the desired authentication method:

  • NODE_REGISTRY_<SCOPE>_AUTH_TOKEN: authentication token
  • NODE_REGISTRY_<SCOPE>_AUTH_BASIC: base64 authentication string (base64(username + ':' + password))

⚠ The <SCOPE> part is the registry-scope transformed in SCREAMING_SNAKE_CASE (uppercase words separated by underscores).

Example:

variables:
  NODE_CONFIG_SCOPED_REGISTRIES: "@public-repo:https://public.npm.registry/some/repo @my-priv-repo:https://private.npm.registry/another/repo"
  # NODE_REGISTRY_MY_PRIV_REPO_AUTH set as a project secret variables

Jobs

node-lint job

The Node template features a node-lint job that performs a code analysis with ESLint. This job is disabled by default. It can be activated by setting NODE_LINT_ENABLED.

It is bound to the build stage, and uses the following variable:

Input / Variable Description Default value
lint-enabled / NODE_LINT_ENABLED Set to true to enable lint analysis none (disabled)
lint-args / NODE_LINT_ARGS npm run script arguments to execute the lint analysis
yarn run script arguments to execute the lint analysis
pnpm run script arguments to execute the lint analysis
run lint

In addition to a textual report in the console, this job produces the following reports, kept for one day:

Report Format Usage
$NODE_PROJECT_DIR/reports/node-lint.gitlab.json GitLab GitLab integration
$NODE_PROJECT_DIR/reports/node-lint.xslint.json JSON ESLint SonarQube integration
Report Format Usage
$NODE_PROJECT_DIR/reports/npm-audit.native.json JSON DefectDojo integration
This report is generated only if DefectDojo template is detected, if needed, you can force it with $DEFECTDOJO_NPMAUDIT_REPORTS

node-build job

The Node template features a job node-build that performs build and tests all at once. You can disable the build using the variable NODE_BUILD_DISABLED

Those stages are performed in a single job for optimization purpose (it saves time) and also for jobs dependency reasons (some jobs such as SONAR analysis have a dependency on test results).

This job is bound to the build stage, and uses the following variables:

Input / Variable Description Default value
build-disabled / NODE_BUILD_DISABLED Set to true to disable build none (enabled)
build-dir / NODE_BUILD_DIR Variable to define build directory dist
build-args / NODE_BUILD_ARGS npm run script arguments
yarn run script arguments
pnpm run script arguments
⚠ default value should be overridden for pnpm as --prod is not a valid option.
run build --prod
test-args / NODE_TEST_ARGS npm test arguments
yarn test arguments
pnpm test arguments
test -- --coverage

Unit Tests and Code Coverage reports

This chapter details the required configuration (depending on the unit testing framework you're using) in order to integrate your unit tests reports and code coverage reports to GitLab.

Additionally, if also using SonarQube, you'll have to enable some extra reporters.

Unit testing with Jest

Here is the required configuration if you're using Jest as unit testing framework.

Reporter Needs npm install Expected report file Usage
jest-junit Yes reports/node-test.xunit.xml GitLab unit tests integration (JUnit format)
istanbul text No N/A (stdout) GitLab MR test coverage results (GitLab grabs coverage from stdout)
istanbul cobertura No reports/cobertura-coverage.xml GitLab code coverage integration (Cobertura format)
jest-sonar Yes reports/node-test.sonar.xml SonarQube unit tests integration (generic SonarQube format)
istanbul lcovonly No reports/lcov.info SonarQube code coverage integration (JS/TS LCOV format)

Here is an example of a jest.config.js configuration file with all the above reporters configured as expected:

  reporters: [
    "default",
    // 'jest-junit' to enable GitLab unit test report integration
    [
      "jest-junit",
      {
          outputDirectory: "reports",
          outputName: "node-test.xunit.xml",
      },
    ],
    // [OPTIONAL] only if using SonarQube
    // 'jest-sonar' to enable SonarQube unit test report integration
    [
      "jest-sonar",
      {
          outputDirectory: "reports",
          outputName: "node-test.sonar.xml",
      },
    ],
  ],
  coverageDirectory: "reports",
  coverageReporters: [
    // 'text' to let GitLab grab coverage from stdout
    "text",
    // 'cobertura' to enable GitLab test coverage visualization
    "cobertura",
    // [OPTIONAL] only if using SonarQube
    // 'lcovonly' to enable SonarQube test coverage reporting
    "lcovonly",
  ],

Unit testing with Mocha

Here is the required configuration if you're using Mocha as unit testing framework.

Reporter Needs npm install Expected report file Usage
mocha-junit-reporter Yes reports/node-test.xunit.xml GitLab unit tests integration (JUnit format)
istanbul text Yes (in nyc package) N/A (stdout) GitLab MR test coverage results (GitLab grabs coverage from stdout)
istanbul cobertura Yes (in nyc package) reports/cobertura-coverage.xml GitLab code coverage integration (Cobertura format)
mocha-sonarqube-reporter Yes reports/node-test.sonar.xml SonarQube unit tests integration (generic SonarQube format)
istanbul lcovonly Yes (in nyc package) reports/lcov.info SonarQube code coverage integration (JS/TS LCOV format)

⚠ Remarks:

  1. By default - unlike Jest - Mocha doesn't provide code coverage. To do so you need to install Istanbul package (nyc):
npm install --save-dev nyc
  1. the default xunit Mocha reporter doesn't produce a JUnit format supported by GitLab, that's why we recommend you to use mocha-junit-reporter instead.
  2. Mocha doesn't support multiple unit tests reporters. So unfortunaltely, if you're using SonarQube, you'll have to choose which report you want to generate. Another option is to use mocha-multi-reporters (see documentation)

Mocha may be either configured with CLI options of using separate Mocha and nyc config files.

Here is the required configuration with CLI options directly in the package.json file:

  "scripts": {
    "test": "npm run mocha",
    "mocha": "nyc --report-dir=reports --reporter=text --reporter=lcovonly --reporter=cobertura mocha --reporter mocha-junit-reporter --reporter-option mochaFile=reports/node-test.xunit.xml test/*.js",
    ...
  },
  ...

Here is the equivalent using separate config files:

  • package.json:
  "scripts": {
    "test": "npm run mocha",
    "mocha": "nyc mocha test/*.js",
    ...
  },
  ...
  • .mocharc.json:

  • with mocha-junit-reporter (for GitLab):

    {
      "reporter": "mocha-junit-reporter",
      "reporter-option": ["mochaFile=reports/node-test.xunit.xml"]
    }
    
  • with mocha-sonarqube-reporter (for SonarQube):

    {
      "reporter": "mocha-sonarqube-reporter",
      "reporter-option": ["output=reports/node-test.sonar.xml"]
    }
    
  • with both (using mocha-multi-reporters):

    {
      "reporter": "mocha-multi-reporters",
      "reporter-option": ["configFile=.mmr.json"]
    }
    

    With .mmr.json:

    {
      "reporterEnabled": "spec, mocha-junit-reporter, mocha-sonarqube-reporter",
      "mochaJunitReporterReporterOptions": {
        "mochaFile": "reports/node-test.xunit.xml"
      },
      "mochaSonarqubeReporterReporterOptions": {
        "output": "reports/node-test.sonar.xml"
      }
    }
    
  • .nycrc.json:

{
  "reporter": ["text", "cobertura", "lcovonly"],
  "report-dir": "reports"
}

Unit testing with Jasmine

Support of Jasmine as unit testing framework is not documented yet and will come soon in a further version of this template.

SonarQube analysis

If you're using the SonarQube template to analyse your Node code, here are 2 sample sonar-project.properties files.

If using JavaScript language:

# see: https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/
# set your source directory(ies) here (relative to the sonar-project.properties file)
sonar.sources=.
# exclude unwanted directories and files from being analysed
sonar.exclusions=node_modules/**,dist/**,**/*.test.js

# set your tests directory(ies) here (relative to the sonar-project.properties file)
sonar.tests=.
sonar.test.inclusions=**/*.test.js

# tests report: generic format
sonar.testExecutionReportPaths=reports/node-test.sonar.xml
# lint report: ESLint JSON
sonar.eslint.reportPaths=reports/node-lint.xslint.json
# coverage report: LCOV format
sonar.javascript.lcov.reportPaths=reports/lcov.info

If using TypeScript language:

# see: https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/
# set your source directory(ies) here (relative to the sonar-project.properties file)
sonar.sources=src
# exclude unwanted directories and files from being analysed
sonar.exclusions=node_modules/**,dist/**,**/*.spec.ts

# set your tests directory(ies) here (relative to the sonar-project.properties file)
sonar.tests=src
sonar.test.inclusions=**/*.spec.ts

# tests report: generic format
sonar.testExecutionReportPaths=reports/node-test.sonar.xml
# lint report: TSLint JSON
sonar.typescript.tslint.reportPaths=reports/node-lint.xslint.json
# coverage report: LCOV format
sonar.typescript.lcov.reportPaths=reports/lcov.info

More info:

node-audit job

The Node template features a job node-audit that performs an audit (npm audit, yarn audit or pnpm audit) to find vulnerabilities (security).

It is bound to the test stage.

Input / Variable Description Default value
audit-disabled / NODE_AUDIT_DISABLED Set to true to disable npm audit none (enabled)
audit-args / NODE_AUDIT_ARGS npm audit arguments
yarn audit arguments
pnpm audit arguments
--audit-level=low

In addition to a textual report in the console, this job produces the following report, kept for one day and only available for download by users with the Developer role or higher:

Report Format Usage
$NODE_PROJECT_DIR/reports/npm-audit.native.json JSON DefectDojo integration
This report is generated only if DefectDojo template is detected, if needed, you can force it with $DEFECTDOJO_NPMAUDIT_REPORTS

node-outdated job

The Node template features a job node-outdated that performs outdated analysis (npm outdated, yarn outdated or pnpm outdated to find dependencies that might be updated.

It is bound to the test stage.

Input / Variable Description Default value
outdated-disabled / NODE_OUTDATED_DISABLED Set to true to disable npm outdated none (enabled)
outdated-args / NODE_OUTDATED_ARGS npm outdated arguments
yarn outdated arguments
pnpm outdated arguments
--long

The job generates an outdated report that you will find here: NODE_PROJECT_DIR/reports/npm-outdated-report.json. This report is only available for download by users with the Developer role or higher

node-semgrep job

The Node template features a job node-semgrep that performs a Semgrep analysis.

It is bound to the test stage, and uses the following variables:

Input / Variable Description Default value
semgrep-disabled / NODE_SEMGREP_DISABLED Set to true to disable this job none
semgrep-image / NODE_SEMGREP_IMAGE The Docker image used to run Semgrep registry.hub.docker.com/semgrep/semgrep:latest
semgrep-args / NODE_SEMGREP_ARGS Semgrep scan options --metrics off --disable-version-check --no-suppress-errors
semgrep-rules / NODE_SEMGREP_RULES Space-separated list of Semgrep rules.
Can be both local YAML files or remote rules from the Semgrep Registry (denoted by the p/ prefix).
p/javascript p/eslint p/gitlab-eslint
semgrep-registry-base-url / NODE_SEMGREP_REGISTRY_BASE_URL The Semgrep Registry base URL that is used to download the rules. No trailing slash. https://semgrep.dev/c
semgrep-download-rules-enabled / NODE_SEMGREP_DOWNLOAD_RULES_ENABLED Download Semgrep remote rules true

ℹ Semgrep may collect some metrics, especially when using rules from the Semgrep Registry. To protect your privacy and let you run Semgrep in air-gap environments, this template disables all Semgrep metrics by default:

  • rules from the Semgrep registry are pre-downloaded and passed to Semgrep as local rule files (can be disabled by setting semgrep-download-rules-enabled / NODE_SEMGREP_DOWNLOAD_RULES_ENABLED to false),
  • the --metrics option is set to off,
  • the --disable-version-check option is set.

In addition to a textual report in the console, this job produces the following reports, kept for one week and only available for download by users with the Developer role or higher:

Report Format Usage
$NODE_PROJECT_DIR/reports/node-semgrep.gitlab.json GitLab's SAST format GitLab integration
$NODE_PROJECT_DIR/reports/node-semgrep.native.json Semgrep's JSON format DefectDojo integration
This report is generated only if DefectDojo template is detected

node-sbom job

This job generates a SBOM file listing installed packages using @cyclonedx/cyclonedx-npm.

It is bound to the test stage, and uses the following variables:

Input / Variable Description Default value
sbom-disabled / NODE_SBOM_DISABLED Set to true to disable this job none
sbom-version / NODE_SBOM_VERSION The version of @cyclonedx/cyclonedx-npm used to emit SBOM none (uses latest)
sbom-opts / NODE_SBOM_OPTS Options for @cyclonedx/cyclonedx-npm used for SBOM analysis --omit dev

node-publish job

This job publishes the project packages to a npm registry.

This job is bound to the publish stage and is disabled by default. When enabled, it is executed on a Git tag with a semantic versioning pattern (v?[0-9]+\.[0-9]+\.[0-9]+, configurable).

It uses the following variables:

Input / Variable Description Default value
publish-enabled / NODE_PUBLISH_ENABLED Set to true to enable the publish job none (disabled)
publish-args / NODE_PUBLISH_ARGS npm publish extra arguments
yarn publish extra arguments
pnpm publish extra arguments
none
🔒 NODE_PUBLISH_TOKEN npm publication registry authentication token none

Configure the target registry

The target registry url for publication shall be configured in the publishConfig of your package.json file. \ If no registry is configured in publishConfig, it will publish to default target registry 'https://registry.npmjs.org/' only if access is set to 'public' in the publishConfig of your package.json file. \ Examples:

  • for an unscoped package:
{
  "name": "my-package",
  // ...
  "publishConfig": {
    "registry": "https://registry.acme.corp/npm"
  }
  // ...
}
  • for a scoped package:
{
  "name": "@acme/my-package",
  // ...
  "publishConfig": {
    "@acme:registry": "https://registry.acme.corp/npm"
  }
  // ...
}

Then simply declare the registry authentication token with 🔒 NODE_PUBLISH_TOKEN.

ℹ it is not mandatory to declare the registry if you wish to use the GitLab project-level npm packages registry (it is declared by default by the template, with the required credentials). All you have to do to is to make sure your npm package name uses the right scope. For example, if your project is https://gitlab.example.com/my-org/engineering-group/team-amazing/analytics, the root namespace is my-org, and your package name must have the @my-org scope (probable package fullname: @my-org/analytics).

Exclude resources from package

Don't forget to exclude undesired folders and files from the package resources (simply add them to your .gitignore or .npmignore file):

  • the .npm/, .yarn/ or .pnpm-store folder, that is used internally by the Node template to store npm, yarn or pnpm cache (depending on the package manager you're actually using),
  • the reports/ folder, that is used by most to be continuous to output all kind of reports,
  • the Node.js build output dir (if any),
  • any other undesired file & folder that you don't want to appear in your published package(s).

Variants

The Node template can be used in conjunction with template variants to cover specific cases.

Vault variant

This variant allows delegating your secrets management to a Vault server.

Configuration

In order to be able to communicate with the Vault server, the variant requires the additional configuration parameters:

Input / Variable Description Default value
TBC_VAULT_IMAGE The Vault Secrets Provider image to use (can be overridden) registry.gitlab.com/to-be-continuous/tools/vault-secrets-provider:latest
vault-base-url / VAULT_BASE_URL The Vault server base API url none
vault-oidc-aud / VAULT_OIDC_AUD The aud claim for the JWT $CI_SERVER_URL
🔒 VAULT_ROLE_ID The AppRole RoleID must be defined
🔒 VAULT_SECRET_ID The AppRole SecretID must be defined

Usage

Then you may retrieve any of your secret(s) from Vault using the following syntax:

@url@http://vault-secrets-provider/api/secrets/{secret_path}?field={field}

With:

Parameter Description
secret_path (path parameter) this is your secret location in the Vault server
field (query parameter) parameter to access a single basic field from the secret JSON payload

Example

include:
  # main template
  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node@4.1.0
  # Vault variant
  - component: $CI_SERVER_FQDN/to-be-continuous/node/gitlab-ci-node-vault@4.1.0
    inputs:
      # audience claim for JWT
      vault-oidc-aud: "https://vault.acme.host"
      vault-base-url: "https://vault.acme.host/v1"
      # $VAULT_ROLE_ID and $VAULT_SECRET_ID defined as a secret CI/CD variable

variables:
  NODE_CONFIG_SCOPED_REGISTRIES: "@public-repo:https://public.npm.registry/some/repo @my-priv-repo:https://private.npm.registry/another/repo"
  # retrieve private repo auth token from Vault
  NODE_REGISTRY_MY_PRIV_REPO_AUTH: "@url@http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/node/priv-repo/creds?field=token"