JS Library 4/7: Continuous Integration

JS Library 4/7: Continuous Integration

 · 9 min read

Note: Check out the entire supplemental source code for this blog series.

Now that we are enforcing conventional commits, we can work towards continuous integration (CI) and, eventually, continuous delivery (CD). CI is the process of automatically merging new code into our main branch. In order to be successful, it’s imperative that we validate the code before it gets merged in. What exactly validation means will vary greatly for different teams, but there are three pieces that are pretty common for most projects: linting, testing, and code reviews. In this tutorial we’ll set up each of these checks and then ensure that code can’t be merged unless it passes all three of these checks.

Linting

Linting is the process of checking code for syntax and style errors. We’ll use the most popular JavaScript linter, eslint.

Let’s install it by running npm install eslint --save-dev.

eslint is highly configurable and we’ll need to add a .eslintrc.js configuration file. I highly recommend using the eslint initialization tool (npm init @eslint/config) to generate a config file for you based on your answers to prompts. After running the tool, the following is my config file:

.eslintrc.js

module.exports = {
  env: {
    browser: true,
    commonjs: true,
    es2021: true,
  },
  extends: [
    'airbnb-base',
  ],
  parserOptions: {
    ecmaVersion: 'latest',
  },
  rules: {
  },
};

We can test the tool by linting our index.js file: npx eslint index.js. After linting, I’m presented with three errors:

/Users/shaar/repos/sample-javascript-library/index.js
  2:1   error  Expected indentation of 2 spaces but found 4   indent
  2:12  error  Unexpected string concatenation                prefer-template
  5:27  error  Newline required at end of file but not found  eol-last

✖ 3 problems (3 errors, 0 warnings)
  3 errors and 0 warnings potentially fixable with the `--fix` option.

I’m given the option to fix these errors by running the same command with the --fix flag. For now I’ll leave the errors so that we can test our CI process.

The last thing we’ll do here is setup another npm script that will lint all of our src js files:

package.json

"scripts": {
  ...
  "lint": "eslint ./src/**.js",
  ...
},

Testing

For testing, we’ll use the popular library jasmine.

Let’s install it by running npm install jasmine --save-dev.

And we’ll initialize a config file by running npx jasmine init. This will create a ./spec/support/jasmine.json file with some default settings.

./spec/support/jasmine.json

{
  "spec_dir": "spec",
  "spec_files": [
    "**/*[sS]pec.?(m)js"
  ],
  "helpers": [
    "helpers/**/*.?(m)js"
  ],
  "env": {
    "stopSpecOnExpectationFailure": false,
    "random": true
  }
}

We’ll need to modify the spec_dir to point to our src folder:

./spec/support/jasmine.json

{
  "spec_dir": "./src",
  ...
}

Now we can create an index.spec.js test file:

./src/index.spec.js

const greeting = require('./index');

describe('greeting', () => {
  it('greets you by name', () => {
    const message = greeting('Steve');
    expect(message).toBe('Hello, Steve.');
  });
});

Now we can run npx jasmine to run all of our tests. Running it gives me the following errors:

Failures:
1) greeting greets you by name
  Message:
    Expected 'Hello, Steve' to be 'Hello, Steve.'.
  Stack:
        at <Jasmine>
        at UserContext.<anonymous> (/Users/shaar/repos/sample-javascript-library/src/index.spec.js:6:21)
        at <Jasmine>

1 spec, 1 failure
Finished in 0.006 seconds

I’m missing a . at the end of my greeting, but I’ll leave this for now so we can test our CI process.

Finally, we’ll alter our npm test script to call jasmine:

./package.json

"scripts": {
  ...
  "test": "jasmine"
},

Enforce Code Reviews

A good practice is to enforce that all code changes go through a code review and are submitted as a pull request. We can configure branch protection within GitHub to enforce that code changes must follow this process and developers cannot simply merge commits into the main branch.

algorithm comparison

Enforce Linting and Testing

We can use github actions to enforce that our code is linted and tested before being merged into our main branch.

To use github actions, we’ll start by creating a build workflow file:

./.github/workflows/build.yml

name: Build
on:
  pull_request:
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 'lts/*'
      - name: Install dependencies
        run: npm ci
      - name: Lint
        run: npm run lint
      - name: Test
        run: npm run test

There’s almost no end to what you can do in a workflow file, but this one has five steps:

  1. checkout the repo
  2. setup Node.js
  3. install dependencies
  4. lint source files
  5. run tests

Pull Requests

Now we’re ready to merge our changes into our main branch, but we won’t be able to just push to main (remember we now require a pull request). So we’ll commit our changes to a new branch:

git add .
git commit -m "chore: add ci"
git checkout -b topic/add-ci
git push -u origin topic/add-ci

In GitHub we’ll now have an option to “Compare & pull request”. We’ll follow that through to make a pull request. After waiting a minute we’ll see that one of our status checks has failed.

warning status check

Right now it will still allow us to merge our changes, so there’s one more setting we need to update. Back in branch protection settings, we’ll need to add Build as a required status check.

add-status-check

Now, when reload the page the “Rebase and merge” button will be disabled.

failed-status-check

We can view the details of our failed build and see that it failed on the Lint step with the following error:

Run npm run lint

> sample-javascript-library@1.0.1 lint
> eslint ./src/**.js


/home/runner/work/sample-javascript-library/sample-javascript-library/src/index.js
Error:   2:10  error  Unexpected string concatenation  prefer-template

✖ 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

Error: Process completed with exit code 1.

We can run the following commands to fix our lint errors, amend our commit, and update the pull request:

npm run lint -- --fix
git add .
git commit --amend --no-edit
git push -f

Our pull request will automatically see this update and rerun the build.

We’ll notice that the build still fails, but this time on the Test step. If we look into the details we can see the following error:

Run npm run test

> sample-javascript-library@1.0.1 test
> jasmine

Randomized with seed 33428
Started
F

Failures:
1) greeting greets you by name
  Message:
    Expected 'Hello, Steve' to be 'Hello, Steve.'.
  Stack:
        at <Jasmine>
        at UserContext.<anonymous> (/home/runner/work/sample-javascript-library/sample-javascript-library/src/index.spec.js:6:21)
        at <Jasmine>

1 spec, 1 failure
Finished in 0.009 seconds
Randomized with seed 33428 (jasmine --random=true --seed=33428)

We can make our test pass by adding a . on line two of our index file:

./src/index.js

...
  return  `Hello, ${name}.`;
...

Now we can run the same four commands above to lint our code, amend our commit, and update the pull request.

This time all of our checks pass and our merge button is green!

status-check-pass

Merging this pull request will update the main branch of our repository, but we will still need to manually deploy to npm to make our changes easily consumable.

Next, we’ll look at how to add continuous deployment to our library.