DEV Community

Cover image for Continuous Integration and Deployment with TravisCI and Netlify
Andrea Stagi
Andrea Stagi

Posted on • Edited on

Continuous Integration and Deployment with TravisCI and Netlify

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, TravisCI and Netlify, 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, this is the final result:

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 selecting Unit Testing, Typescript and Linter/Formatter, and use Jest for testing

vue create github-netlify
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

We only need to test fetchRepositories method, mocking axios requests with some fixture (they're very long, you can see the fixtures here)!

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

To run tests, execute

yarn test:unit
Enter fullscreen mode Exit fullscreen mode

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

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

And run tests again to see Code Coverage in action!

➜  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
Enter fullscreen mode Exit fullscreen mode

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 and Codecov on the repository and add .travis.yml file to configure how TravisCI will work

language: node_js
node_js:
  - 10
before_script:
  - yarn add codecov
script:
  - yarn test:unit
after_script:
  codecov
Enter fullscreen mode Exit fullscreen mode

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 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, 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 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

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

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
      });
    });
}
Enter fullscreen mode Exit fullscreen mode

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.

To run lambda functions locally, install Netlify CLI

sudo npm install netlify-cli -g
Enter fullscreen mode Exit fullscreen mode

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

[dev]
  command = "yarn serve"
  functions = "lambda"
Enter fullscreen mode Exit fullscreen mode

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

netlify dev
Enter fullscreen mode Exit fullscreen mode

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

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

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

  // ...
  axios.get(`${process.env.VUE_APP_BACKEND_ENDPOINT}/getmyrepos?page=${this.page}`)
    .then((resp) => {
      this.repositories = this.repositories.concat(resp.data);
      this.page += 1;
    })
  // ...
Enter fullscreen mode Exit fullscreen mode
// ...
(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});
  }
});
// ...
Enter fullscreen mode Exit fullscreen mode

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

export declare function handler(event: any, context: any, callback: any): any;
Enter fullscreen mode Exit fullscreen mode
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();
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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.

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.

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

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

[dev]
  command = "yarn serve"
Enter fullscreen mode Exit fullscreen mode

Check the documentation 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.

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 or you can use a Netlify instance to preview our site and another instance made with Netlify 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 on a specific branch, configuring deployment to act only on that branch (this configuration requires a Netlify token, you can create one following this article).

Top comments (0)