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
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>
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>
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);
});
});
To run tests, execute
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
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!
β 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 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
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
});
});
}
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
And add netlify.toml
file in the root of the project
[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
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
// ...
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
// ...
axios.get(`${process.env.VUE_APP_BACKEND_ENDPOINT}/getmyrepos?page=${this.page}`)
.then((resp) => {
this.repositories = this.repositories.concat(resp.data);
this.page += 1;
})
// ...
// ...
(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.
export declare function handler(event: any, context: any, callback: any): any;
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.
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"
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)