10 Must-Have Continuous Integration Steps for JavaScript and NodeJS
By: Markus Hanslik
Tags: Continuous Integration, JavaScript, Best Practices, Nodejs, Typescript

Photo by Xavier von Erlach on Unsplash

Whether you have a small frontend service, a big monolith, are building an MVP or a production grade application — having a Continuous Integration service running for commits will help your development speed and quality greatly.

Continuous Integration services nowadays are very easy to set up, support Dockerfiles, and are very cheap. They support picking up every code change or specific branches, and running your scripts so that you can implement checks for every code you commit and will never forget about running your unit tests. Especially if you commit often, it’s a great way to see which change broke something.

But what kind of checks should your CI run? The following scripts aim at JavaScript (and/or NodeJS) developers, though the best practices apply to almost any language and service.

1 | Check for security issues of third-party packages

To ensure you are aware of potential security issues and can mitigate them, you can run NPM’s audit command. It searches issues the third-party packages you are using (even those your third-party packages are using) that have been filed in their database, which you can see at https://www.npmjs.com/advisories . This means it only includes issues that have been reported by users.

The security levels are following CVSS (https://www.first.org/cvss/specification-document), meaning to run your audit and exit with code 1 on all high or critical issues, you could implement the following step in your CI:

npm audit --audit-level=high

This will scan your packages and output all issues that have been found. Depending on the number of your dependencies, the list of issues found may be very long; if you only want to see high and critical issues, you can use the following command:

npm audit --audit-level=high | grep -E "(High | Critical)" -B3 -A10

(found at https://github.com/npm/npm/issues/20596)

Aside from NPM (or Yarn) Audit, there are more ways to search for security issues. Generally speaking, you should aim to cover as much of your infrastructure as possible: from your own code to the full stack your code is working on.

For instance, if you are using Docker in production, you can run tools like Clair (https://github.com/quay/clair) to statically analyze your Docker images.

Especially if you are having B2B customers, having security checks is important to pass their audits. You can read more about what happens during B2B security audits at https://medium.com/@Markus.Hanslik/what-a-customers-it-security-audit-for-web-apps-is-and-how-to-pass-it-b637c9df67a1 .

2 | Check for legal issues

Are you developing proprietary software which you don’t want to open source? Then you should definitely make sure you comply with the licenses of the third-party packages you are using. After all, if you are building closed source, you are not allowed to use some licenses like GPL-2.

The NPM package license-checker iterates through all of the NPM packages you are using and can fail on invalid licenses, making it a perfect step for your CI.

3 | Check for outdated dependencies

Especially in the short-lived JavaScript world, packages get updates very frequently, depending on the package some even multiple times per day.

There are multiple ways to have your CI report outdated packages so that you can update them; the simplest way is to use an NPM package.

npm-check-updates does exactly what the name suggests — it checks if any of your third-party NPM dependencies are outdated and can then throw an exit code so that your CI can make you aware that you need updates:

ncu --errorLevel 2

(If you are planning on updating packages manually, you may want to set your CI step to treat errors from NCU as warnings, so that your pipeline is not blocked but you are still aware that updates need to be done.)

Aside from this very manual approach, there are also tools which automate the process of checking for updates and even support checking for outdated libraries in Dockerfiles and more. Check out Renovate (https://www.whitesourcesoftware.com/free-developer-tools/renovate/) or Snyk (https://snyk.io), which also supports security audits.

4 | Lint your code to find coding style and quality issues

If you are working in a team, or even if you are just working on a project across a longer time span, chances are coding styles will deviate, making your code hard to read. Manually checking this takes time and makes code reviews slower.

However, you can use prettier (https://prettier.io) to make sure all of your files are always automatically formatted the same way.

If you want to fail your CI pipeline if your code isn’t correctly formatted, just use the following command (after installing prettier via NPM):

prettier --check “src/**/*.*“ --ignore-unknown

The ignore-unknown flag will ensure that prettier only checks files it can actually format, such as JavaScript, TypeScript, HTML or even YaML.

If your CI supports committing code during a step, you can also just directly fix all errors:

prettier "**/*" --write --ignore-unknown

Additionally, you can use eslint (https://eslint.org) to lint your code. It now also supports TypeScript, so there’s no more need to install TSLint; and using the recommended ESLint rules will help you to find potential issues in your code (e.g. a getter function which doesn’t return anything), and to follow best practices (e.g. don’t assign values in conditional expressions to increase readability).

Once you’ve installed ESLint via NPM and set it up, you can use the following script in your CI step:

npx eslint src --ext .js --format junit > eslint.xml

This will run ESLint on files in your src folder with the js extension and output its findings in the JUnit format, which can be parsed by most CI systems.

By the way, if you want to run several scripts in one CI job, but not immediately fail, you can combine exit codes like so:

- RESULT=0

- npx prettier -c ‘src/**/*.*‘ — Ignore-unknown || RESULT=$((RESULT + $?)) || true

- npx eslint src — ext .js — format junit > eslint.xml || RESULT=$((RESULT + $?)) || true

- if [ $RESULT -gt 0 ]; then exit 1; fi

5 | Build and test - in your container!

Does your CI pipeline support running Docker containers? If so, to reduce room for error, and to reduce code duplication, do not run tests in your CI’s standard runner image directly, but in the actual Docker image you are using for your application.

For instance, if you use the Bitbucket Pipelines CI system, by default a test step could look like this (taken from https://support.atlassian.com/bitbucket-cloud/docs/javascript-nodejs-with-bitbucket-pipelines/):

image: node:10.15.0

pipelines:

default:

- step:

script:

- npm install

- npm test

However, this will test your code in the vanilla NodeJS image. If you are developing a backend application that does some image manipulation, your code may use ImageMagick, and fail here; even though your actual production Docker image might install ImageMagick correctly.

Another scenario setting you up for failure is if you are updating your production image’s Node version but forget to do so in your CI; you would get passed tests in your CI but your code might fail in production.

Running the tests inside your actual container could look like this:

# First, build the container

- docker build -t $IMAGE_NAME:$IMAGE_TAG .

# Then, run the tests inside your container

- docker run — name $IMAGE_NAME-tester-$IMAGE_TAG $IMAGE_NAME:$IMAGE_TAG sh -c “npm run test“

# Finally, copy the test results from inside your container to the CI, so that you may use it as an artifact

- mkdir -p artifacts

- docker cp $IMAGE_NAME-tester-$IMAGE_TAG:/usr/app/src/test-reports ./artifacts/

- docker rm -f $IMAGE_NAME-tester-$IMAGE_TAG

6 | Tag your containers correctly

If you build containers in your CI pipeline, make sure to tag them not just as latest . It will help deploying them; but it will also ensure that you can re-run pipelines without breaking things.

If you only use the latest tag, and then re-run an older pipeline, you will accidentally replace your actual latest image; hence, you should aim to use semantic versioning where possible (e.g. on your release tags) and otherwise tag using your branch names along with the pipeline ID.

That way, you can always re-run any pipeline without overwriting any tag, and you can refer to any image easily.

7 | Add integration tests

Your service likely isn’t completely stand-alone and consumes other services. To ensure that these all work together, you can write and run integration tests in your CI pipeline.

Using Docker, you could even spin up multiple containers — your critical dependencies — without needing to mock everything, and then run Jest, or more sophisticated stacks like Puppeteer or Cypress tests (for web applications).

8 | Run your tests the ‘fail fast’ way

The more steps and tests you will add to your CI, the longer a single run will take. This will almost always result in developers committing less often, which results in the CI running less often, which results in failures being hard to pin down due to big commits.

That shouldn’t stop you from adding many tests to automate as much as possible and efficient; but it should mean that you group and run your tests in the most efficient way.

Can you specify key tests that fail before anything else fails, or that must be passed? If so, you should aim to run those first, so that developers get fast feedback. If those tests pass, the others can then be run afterwards.

If you have many tests, do you need to run all checks and tests on any branch, on any commit? Or is it sufficient to run some of them only during nights? If your code is structured well enough, can you run tests of the module/service you’ve changed first and the rest afterwards?

9 | Export artifacts as JUnit files

Try and use a CI that supports parsing the JUnit format for test results. It is much easier to just open a merge request and get a parsed overview of all test errors and warnings, rather than having to open each failed CI step and scroll in console output. Good developer experience means higher output!

JUnit nowadays is so wide-spread that most tools can generate them out of the box or with very little setup. ESLint, Jest, Cypress, and many more can output as Junit XML files; and CI pipelines such as GitLab can parse them and show them in the CI pipeline branch’s merge request automatically.

Additionally, try to export additional KPIs if you have them, so that you can see their changes over time. For instance, if you value code coverage, pass it to your CI; or if you care about feature coverage, calculate it for your CI and pass that instead.

For GitLab, parsing Jest Junit files along with code coverage would look like this in a CI step:

Test:

script:

- npm run test # ...

artifacts:

name: Test results

paths:

- artifacts/

expire_in: 4 weeks

reports:

junit:

- artifacts/*.xml

coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/

10 | Deploy using your CI

Deploy via your CI to further reduce room for error. It will give you the ability to re-play deployments to revert changes (even if you don’t have blue/green deployments at first), it will get rid of error-prone manual deployment ‚todo‘ lists, and once set up, it greatly increases release confidence.

Additionally, you can keep your documentation updated more easily without forgetting anything.

Assuming you use a tool for error reporting such as Bugsnag, you can push release information after each deployment to Bugsnag so that your error dashboard is kept up-to-date.

Do you have a CHANGELOG.md file? You can grep the changes of the version pushed and announce them in your team channels.

Gain Access to Expert View — Subscribe to DDI Intel