Continuous Integration e Continuous Deployment con TravisCI e Netlify

Parole
Andrea Stagi
Tempo di lettura
7

Vedremo com’è facile creare una pipeline di Continuous Integration e Continuous Deployment (CI/CD) per una app Vue utilizzando TravisCI e Netlify

I

TravisCI è un famoso servizio di continuous integration usato per testare e buildare le applicazioni e integrato con GitHub.

Netlify è un servizio di cloud hosting per i siti statici, integra il processo di continuous deployment, fornisce un certificato SSL, funzioni serverless, e molto altro... Useremo Netlify con GitHub per deployare il nostro sito ogni volta che pushiamo del nuovo codice.

Creiamo la app

Partiamo dalla creazione di una semplice app Vue per visualizzare tutti i nostri repository su Github. Potete visualizzare il codice qui, e questo sarà il risultato finale:

Gli utenti potranno cliccare sulla tile del repository per navigarci dentro o caricare altri repository da visualizzare.

Creiamo quindi la app con Vue CLI selezionando le opzioni di Unit Testing, Typescript e Linter/Formatter, e utilizziamo il framework Jest per il testing

vue create github-netlify

Andiamo adesso a creare il componente Repository che renderizza la box del repository con url, nome e descrizione

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

Nel componente principale App (App.vue) chiamiamo la url Github https://api.github.com/users/USERNAME/repos per fetchare tutti i repository appartenenti ad un dato user e renderizziamoli con il componente Repository. Per rendere l'app facilmente configurabile, memorizziamo lo username in una variabile di ambiente e dichiariamola all'interno del file .env come VUE_APP_GITHUB_USER=astagi. Sia Netlify che Vue supportano il file .env perciò possiamo usarlo tranquillamente per memorizzare tutte le variabili di cui necessitiamo durante lo sviluppo! (Ricordiamoci di aggiungere .env a .gitignore)

Il risultato di questa chiamata è una lista paginata di repository, per supportare le pagine aggiungiamo un pulsante "Load more" che utilizzerà il parametro 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>

Andiamo a testare adesso il metodo fetchRepositories, mockando le richieste fatte con la libreria axios con qualche fixture (sono veramente lunghe, qua le fixture complete)!

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);
  });
});

Lanciamo i test

yarn test:unit

In aggiunta ai test è necessario aggiungere il Code Coverage, ovvero la misura di quante linee, branch, istruzioni del nostro codice vengono eseguite durante il test. Attiviamo il 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/**"]
}

E lanciamo di nuovo i test per vedere il Code Coverage riportato

➜  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

Aggiungere la Continuous Integration con TravisCI

Adesso è arrivato il momento di setuppare TravisCI per la Continuous Integration! Attiviamo la TravisCI Github integration and Codecov sul repository e aggiungiamo il file .travis.yml file per configurare come TravisCI si comporterà

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

Ogni volta che pushiamo del codice sul nostro repository, TravisCI installerà il pacchetto codecov per comunicare con il servizio Codecov (before_script) e lancerà i test in automatico (script), inviando i dati di coverage a Codecov (after_script).

Aggiungere funzionalità backend

Chiamare Github API directly dal componente non è il miglior modo per fetchare tutti i repository. Come si evince dalla documentazione delle Github API c'è un endpoint molto più bello da utilizzare per prendere i repository personali con un rate limit più alto, https://api.github.com/user/repos, ma necessita di un token di autenticazione per funzionare. Generare un nuovo token da Github è facilissimo, ma necessita di rimanere privato e non può essere esposto nel codice frontend, per questo necessitiamo di una parte backend per comunicare con Github. Fortunatamente grazie alle Netlify Function puoi lanciare delle AWS's serverless Lambda function per far girare codice server-side senza avere un server dedicato o un account AWS, tutto gestito in modo trasparente da Netlify. Qua potete trovare la documentazione.

Usare le lambda function con Netlify è veramente facile: basta aggiungere una cartella lambda nella root del progetto e un file getmyrepos.js dove risiedono le funzioni, nel nostro caso:

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
      });
    });
}

Esportiamo poi un metodo handler method wper comunicare con l'endpoint delle Github API usando axios, aggiungendo il nostro token Github (presente nella variabile d'ambiente GITHUB_TOKEN) negli header e quindi mandiamo la risposta attraverso la funzione callback! Necessitiamo inoltre anche del parametro event.queryStringParameters per prendere la page. Per ulteriori informazioni rimando alla documentazione.

Per lanciare le funzioni lambda localmente, installa la 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"

Questo file contiene la configurazione dell'ambiente dev: le funzioni lambda sono collocate nella cartellalambda e il comando per eseguire l'app frontend è yarn serve. Per eseguire l'intera app in modalità dev, aggiungi GITHUB_TOKEN al file.env e avvialo

netlify dev

La nostra app Vue ora gira su http://localhost: 8080 e la funzione lambda su http://localhost: 34567/getmyrepos. È tempo di modificare il codice dell'app e i test per integrare la funzione lambda! Prima di tutto aggiungiamo l'intestazione Access-Control-Allow-Origin = * alla risposta della funzione quando l'app è in esecuzione in modalità dev (la variabile di ambiente NETLIFY_DEV è 'vera') perché l'app Vue e il servizio lambda sono esposti su porte diverse

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

Imposta una nuova variabile d'ambiente VUE_APP_BACKEND_ENDPOINT = http://localhost:34567 per definire il nostro endpoint back-end e modificare l'URL per recuperare i repository nel componenteApp.vue e test

  // ...
  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});
  }
});
// ...

Anche le funzioni Lambda sono facili da testare! Testiamo la nostra funzione aggiungendo la definizione lambda/getmyrepos.d.ts per supportare 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();
    });
  });
});

Ricoriamoci di aggiungere "lambda/*.js" a collectCoverageFrom nel file jest.config.js.

Aggiungere il Continuous Deployment con Netlify

È tempo di deployare il nostro sito su Netlify! Dopo il login fare click su New site from Git e aggiungere il repository.

È possibile configurare il ramo di produzione, creare comandi, funzioni direttamente su Netlify o aggiungerli a netlify.toml. Il modo più semplice per configurare un progetto per CD è usare un ramo protetto chiamato "produzione" e configurare Netlify per iniziare a costruire e pubblicare solo quando viene inviato un nuovo commit su questo ramo. In Impostazioni di build avanzate inImpostazioni -> Build & Deploy devi impostare le variabili di ambiente, ad esempio le modifiche VUE_APP_BACKEND_ENDPOINT nella produzione:/ .netlify / funzioni.

Puoi usare inoltre il file netlify.toml file per configurare le opzioni di build

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

Per ogni pull request fatta, Netlify deploya una preview del sito, accessibile dai dettagli della PR.

Adoro usare questo flusso, posso controllare l'intero sito usando l'anteprima prima di mandare tutto in produzione, ma ci sono moltissimi modi alternativi per configurare il processo di Continuous Deployment! Non mancate per la seconda parte nella quale illustreremo tali metodi

Analizza la tua presenza online.

Scrivici per una consulenza gratuita



Richiedi consulenza