Continuous Integration and Deployment with TravisCI and Netlify
Tags: devops, javascript, tutorial, vue
Published: 2020-04-17 20:43:08

The Continuous Integration/Continuous Deployment (CI/CD) pipeline is an automated sequence of events that you would otherwise need to perform manually: previewing your in-development site, testing your new code and deploying it live! In this tutorial we'll learn how to build a simple `Vue app` and deploy it using CI/CD with [Github](https://github.com/), [TravisCI](https://travis-ci.com/) and [Netlify](https://www.netlify.com/), providing backend functionalities with `Netlify Functions`!

TravisCI is a hosted, distributed continuous integration service used to build and test projects on GitHub.

Netlify offers cloud hosting for static websites, providing continuous deployment, free SSL, serverless functions, and more... We will use Netlify in conjunction with GitHub to deploy our site ever ytime we push new code.

## Create the app

Let's start creating a simple Vue app to display our personal repositories hosted on Github. Check the code [here](https://github.com/astagi/github-netlify/), this is the final result:

<img src="https://github.com/astagi/github-netlify/blob/master/assets/gitdemo.png?raw=true" width=450>

Users can click on tiles to navigate to the selected repository or click "Load more" to fetch other repositories.

Create the project with [Vue CLI](https://cli.vuejs.org/) selecting Unit Testing, Typescript and Linter/Formatter, and use Jest for testing

```sh
vue create github-netlify
```

Create a component `Repository` that renders a box with repository url, name and description

```vue
<template>
  <a :href="repository.html_url">
    <h2>{{repository.name}}</h2>
    <p>{{repository.description}}</p>
  </a>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class Repository extends Vue {
  @Prop() private repository!: any;
}
</script>
```

In the main App component (`App.vue`) call the Github endpoint `https://api.github.com/users/USERNAME/repos` to fetch all the public repositories belonging to a specific user and render them using the `Repository` component. To make the app configurable, store the username in an environment variable and declare it in `.env` file as `VUE_APP_GITHUB_USER=astagi`. Both Netlify and Vue support `.env` file so we can use it to store all the environment variables we need during local development! (Remember to add `.env` to `.gitignore`)

The result of this API call is a paginated list of repositories, to support pages add a button to load more pages and use the query parameter `page`.

```vue
<template>
  <div id="app">
    <h1>My Github repositories</h1>
    <Repository v-for="repository of repositories" :key="repository.id" :repository="repository"/>
    <button v-on:click="fetchRepositories()" :disabled="!loadMoreEnabled">Load more</button>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import axios from 'axios';
import Repository from './components/Repository.vue';

@Component({
  components: {
    Repository,
  },
})
export default class App extends Vue {

  private repositories: any[] = [];
  private page = 1;
  private loadMoreEnabled = true;

  private mounted() {
    this.fetchRepositories();
  }

  public fetchRepositories() {
    this.loadMoreEnabled = false;
    axios.get(`https://api.github.com/users/${process.env.VUE_APP_GITHUB_USER}/repos?page=${this.page}`)
      .then((resp) => {
        this.repositories = this.repositories.concat(resp.data);
        this.page += 1;
      })
      .finally(() => {
        this.loadMoreEnabled = true;
      });
  }
}
</script>
```

We only need to test `fetchRepositories` method, mocking axios requests with some fixture (they're very long, you can see the fixtures [here](https://github.com/astagi/github-netlify/tree/master/tests/__fixtures__))!

```ts
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'
import App from '@/App.vue';
import reposResponses from '../__fixtures__/reposResponses';
import axios from 'axios'


jest.mock("axios");

(axios.get as jest.Mock).mockImplementation((url) => {
  switch (url) {
    case `https://api.github.com/users/${process.env.VUE_APP_GITHUB_USER}/repos?page=1`:
      return Promise.resolve({data : reposResponses.page1});
    case `https://api.github.com/users/${process.env.VUE_APP_GITHUB_USER}/repos?page=2`:
      return Promise.resolve({data : reposResponses.page2});
  }
});

describe('App.vue component', () => {
  let wrapper: any;
  beforeEach(() => {
    wrapper = shallowMount(App);
  });
  it('renders repositories on mount', async () => {
    await Vue.nextTick();
    expect(wrapper.findAll('repository-stub').length).toEqual(30);
  });
  it('fetches other repositories on load more', async () => {
    await Vue.nextTick();
    wrapper.vm.fetchRepositories();
    await Vue.nextTick();
    expect(wrapper.findAll('repository-stub').length).toEqual(60);
  });
});
```

To run tests, execute

```sh
yarn test:unit
```

In addition to tests we need to setup `Code Coverage`, a measurement of how many lines, branches, statements of our code are executed while the automated tests are running. Activate `code coverage` in `jest.config.js`

```js
module.exports = {
  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
  collectCoverage: true,
  collectCoverageFrom: ["src/**/*.vue", "!**/node_modules/**"]
}
```

And run tests again to see Code Coverage in action!

```sh
➜  github-netlify (master) ✗ yarn test:unit
yarn run v1.19.2
$ vue-cli-service test:unit
 PASS  tests/unit/app.spec.ts
 PASS  tests/unit/lambda.spec.ts
-----------------|----------|----------|----------|----------|-------------------|
File             |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files        |      100 |      100 |      100 |      100 |                   |
 src             |      100 |      100 |      100 |      100 |                   |
  App.vue        |      100 |      100 |      100 |      100 |                   |
 src/components  |      100 |      100 |      100 |      100 |                   |
  Repository.vue |      100 |      100 |      100 |      100 |                   |
-----------------|----------|----------|----------|----------|-------------------|

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        6.878s
```

Remember to add `coverage` folder to `.gitignore`!

## Add Continuous Integration with TravisCI

Now that our code has tests, coverage and the minimal functionalities ready, it's time to setup TravisCI for Continuous Integration! Activate [TravisCI Github integration](https://github.com/marketplace/travis-ci) and [Codecov](https://github.com/marketplace/codecov) on the repository and add `.travis.yml` file to configure how TravisCI will work

```yaml
language: node_js
node_js:
  - 10
before_script:
  - yarn add codecov
script:
  - yarn test:unit
after_script:
  codecov
```

Every time we push code on the repository, TravisCI will install `codecov` package to communicate with Codecov (`before_script`) and run tests for you (`script`), sending coverage data to Codecov (`after_script`).

## Add backend functionalities

Calling Github API directly from the component is not the best way to get all the repositories. As you can read from [Github API docs](https://developer.github.com/v3/repos/#list-your-repositories) there is a better API endpoint to get personal repositories with a higher rate limit, `https://api.github.com/user/repos`, but it needs an authentication token to work. [Getting a new token from Github is easy](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line), but it must be kept secret and can't be exposed in frontend code, so we need a backend server to communicate with Github. Fortunately with `Netlify Functions` you can run [AWS's serverless Lambda functions](https://aws.amazon.com/lambda/) to run server-side code without having a dedicated server or an AWS account, with function management handled directly within Netlify. For more info have a look at [Netlify Functions documentation](https://docs.netlify.com/functions/overview/#manage-your-serverless-functions)

Setting up a lambda function with Netlify is really easy: add a folder called `lambda` to the root of the project and a file `getmyrepos.js` where the function resides

```js
const axios = require('axios');


exports.handler = function(event, context, callback) {
  let responseHeaders = {
    'Content-Type': 'application/json'
  };
  if (process.env.NETLIFY_DEV === 'true') {
    responseHeaders['Access-Control-Allow-Origin'] = '*';
  }
  axios.get(`https://api.github.com/user/repos?visibility=public&page=${event.queryStringParameters.page}`, {
      headers : {
        'Authorization': `token ${process.env.GITHUB_TOKEN}`
      }
    })
    .then((response)  => {
      callback(null, {
        statusCode: 200,
        body: JSON.stringify(response.data),
        headers: responseHeaders
      });
    })
    .catch((error) => {
      callback(null, {
        statusCode: error.response.status,
        body: JSON.stringify({'message' : error.response.data.message}),
        headers: responseHeaders
      });
    });
}
```

We just need to export a `handler` method where we communicate with the Github API endpoint using axios, adding our Github token (stored in the environment variable `GITHUB_TOKEN`) to the headers and then return the response using `callback` function provided by Netlify! We also need `event.queryStringParameters` object to get query parameters, in this case `page`. For more info see [how to build serverless functions in Netlify with JavaScript](https://docs.netlify.com/functions/build-with-javascript/#format).

To run lambda functions locally, install [Netlify CLI](https://github.com/netlify/cli)

```sh
sudo npm install netlify-cli -g
```

And add `netlify.toml` file in the root of the project

```ini
[dev]
  command = "yarn serve"
  functions = "lambda"
```

This file contains the `dev` environment configuration: lambda functions are placed in `lambda` folder and the command to run frontend app is `yarn serve`. To run the entire app in dev mode add `GITHUB_TOKEN` to the `.env` file and launch

```sh
netlify dev
```

Our Vue app now runs at `http://localhost:8080` and lambda function at `http://localhost:34567/getmyrepos`. It's time to modify the app code and tests to integrate lambda function in our app! First of all add `Access-Control-Allow-Origin=*` header to the function response when the app is running in dev mode (`NETLIFY_DEV` environment variable is 'true') because Vue app and lambda service are exposed on different ports

```js
  // ...
    let responseHeaders = {
      'Content-Type': 'application/json'
    };
    if (process.env.NETLIFY_DEV === 'true') {
      responseHeaders['Access-Control-Allow-Origin'] = '*';
    }
  // ...
```

Setup a new environment variable `VUE_APP_BACKEND_ENDPOINT=http://localhost:34567` to define our backend endpoint and change the url to fetch repositories in the `App.vue` component and tests

```ts
  // ...
  axios.get(`${process.env.VUE_APP_BACKEND_ENDPOINT}/getmyrepos?page=${this.page}`)
    .then((resp) => {
      this.repositories = this.repositories.concat(resp.data);
      this.page += 1;
    })
  // ...
```

```ts
// ...
(axios.get as jest.Mock).mockImplementation((url) => {
  switch (url) {
    case `${process.env.VUE_APP_BACKEND_ENDPOINT}/getmyrepos?page=1`:
      return Promise.resolve({data : reposResponses.page1});
    case `${process.env.VUE_APP_BACKEND_ENDPOINT}/getmyrepos?page=2`:
      return Promise.resolve({data : reposResponses.page2});
  }
});
// ...
```

Lambda functions are easy to test as well! Let's test our function adding `lambda/getmyrepos.d.ts` definition to support TypeScript.

```ts
export declare function handler(event: any, context: any, callback: any): any;
```

```ts
import reposResponses from '../__fixtures__/reposResponses';
import axios from 'axios';
import { handler } from '@/../lambda/getmyrepos';


jest.mock('axios');

(axios.get as jest.Mock).mockImplementation((url) => {
  switch (url) {
    case `https://api.github.com/user/repos?visibility=public&page=1`:
      return Promise.resolve({data : reposResponses.page1});
    case `https://api.github.com/user/repos?visibility=public&page=2`:
      let err: any = {}
      err.response = {
        status: 401,
        data: {
          message: 'Bad Credentials'
        }
      }
      return Promise.reject(err)
  }
});

describe('Lambda function getmyrepos', () => {
  it('renders repositories on call page 1', (done) => {
    const event = {
      queryStringParameters : {
        page : 1,
      },
    };
    handler(event, {}, (e: any, obj: any) => {
      expect(obj.statusCode).toEqual(200);
      expect(obj.body).toEqual(JSON.stringify(reposResponses.page1));
      done();
    });
  });
  it('shows message error if any', (done) => {
    const event = {
      queryStringParameters : {
        page : 2,
      },
    };
    handler(event, {}, (e: any, obj: any) => {
      expect(obj.statusCode).toEqual(401);
      expect(obj.body).toEqual(JSON.stringify({message: 'Bad Credentials'}));
      done();
    });
  });
  it('renders repositories with Access-Control-Allow-Origin * in dev mode', (done) => {
    process.env.NETLIFY_DEV = 'true';
    const event = {
      queryStringParameters : {
        page : 1,
      },
    };
    handler(event, {}, (e: any, obj: any) => {
      expect(obj.headers['Access-Control-Allow-Origin']).toEqual('*');
      done();
    });
  });
});
```

Remember to add `"lambda/*.js"` to `collectCoverageFrom` in `jest.config.js`.

## Add Continuous Deployment with Netlify

It's time to publish our site with Netlify! After logging in, click `New site from Git` and add the repository. 

<img src="https://github.com/astagi/github-netlify/blob/master/assets/netlify_start.png?raw=true" width=450>

You can configure production branch, build commands, functions directly on Netlify or add them to `netlify.toml`. The simplest way to configure a project for CD is using a protected branch called `production` and configure Netlify to start building and publishing only when a new commit gets pushed on this branch. In `Advanced build settings` under `Settings -> Build & Deploy` you have to set environment variables, for example the `VUE_APP_BACKEND_ENDPOINT` changes in production: `/.netlify/functions`.

<img src="https://github.com/astagi/github-netlify/blob/master/assets/netlify_env.png?raw=true" width=450>

You can also use `netlify.toml` file to configure the build settings

```ini
[build]
  base = "/"
  publish = "dist"
  command = "yarn build"
  functions = "lambda"

[dev]
  command = "yarn serve"
```

[Check the documentation](https://docs.netlify.com/configure-builds/file-based-configuration/) for more settings available in file-based configuration.

For every pull request you make, Netlify deploys a site preview, accessible directly from the PR details.

<img src="https://github.com/astagi/github-netlify/blob/master/assets/netlify_pr.png?raw=true" width=450>

I love using this flow, I can check the entire site using the preview before merging to `production`, but there are alternative ways to configure Continuous Deployment! For example you can setup a custom CD flow triggering builds using a [build hook](https://docs.netlify.com/configure-builds/build-hooks/) or you can use a Netlify instance to preview our site and another instance made with [Netlify Drop](https://app.netlify.com/drop) for production, where you can host a site just dropping files on Netlify. Then [configure TravisCI to build and deploy our site after tests pass](https://docs.travis-ci.com/user/deployment-v2/providers/netlify/) on a specific branch, [configuring deployment to act only on that branch](https://docs.travis-ci.com/user/deployment-v2/conditional) (this configuration requires a Netlify token, you can create one following [this article](https://docs.netlify.com/cli/get-started/#obtain-a-token-in-the-netlify-ui)).