DEV Community

Nathally Souza
Nathally Souza

Posted on

Construindo uma pipeline com o Github Actions

Que o Github é uma plataforma extremamente útil, todas nós sabemos, mas dentre uma de suas funcionalidades uma das que mais me fascina é o Github Actions. Com as Actions nós podemos construir uma pipeline para nossas aplicações desde as mais simples, até se precisarmos de uma complexidade maior com banco de dados.

Antes de mergulharmos nesse processo, é necessário entender o que é uma pipeline. Dentro do universo de desenvolvimento, uma pipeline é onde podemos visualizar as validações antes de entregar nossa aplicação em produção. É nessa etapa onde rodamos nossos testes automatizados para garantir que as alterações, que estão sendo enviadas para produção, não irão gerar nenhum erro.

Com este primeiro conceito, agora entra uma segunda etapa, uma pipeline bem configurada, permitindo que tenhamos um ambiente de stage, ou homologação, e o nosso ambiente de produção. A ideia é que no ambiente de stage sejam feitos testes manuais antes de enviar todas as mudanças para produção.

Quando qualquer erro é encontrado, a pipeline impede o deploy para qualquer um dos ambientes e é necessário que realizemos as correções antes do deploy. No exemplo abaixo temos o caso de uma pipeline que teve falha em uma das verificações dos testes unitários e bloqueou a mudança em stage.

Image description

Image description

No caso do exemplo exibido, é necessário fazer a correção na mensagem retornada para que as validações sejam feitas e o deploy possa seguir em cada um dos ambientes. Se tudo der certo, a aplicação será disponibilizada em todos os ambientes após os testes automatizados.

Image description

Neste cenário, temos a situação em que todas as validações passaram e conseguimos disponibilizar nossa aplicação no ambiente final. Depois desses conceitos, como podemos realizar todo esse processo usando o Github Actions?

No nosso exemplo temos uma aplicação simples com Nest, então usaremos as definições fornecidas pelo Github para aplicações com Node.

Na raiz do nosso projeto, criamos uma pasta .github, dentro dela um repositório workflow, onde colocaremos nosso
Image descriptionarquivo main.yml. É este arquivo yaml que será o responsável por definir quais são as etapas que serão percorridas em nossa pipeline.

No nosso exemplo o nosso arquivo yaml está da seguinte forma

name: Development

on:
  push:
    branches:
      - main
      - stage
  pull_request:
    branches:
      - main
      - stage

env:
  PORT: 5000

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: yarn --frozen-lockfile

    - name: Run unit tests
      run: yarn test

  e2e:
    if: ${{ always() && contains(join(needs.*.result, ','), 'success') }}
    needs: [test]
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: yarn --frozen-lockfile

    - name: Running e2e tests
      run: yarn test:e2e

  stage:
    if: ${{ always() && contains(join(needs.*.result, ','), 'success') }}
    needs: [test, e2e]
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: yarn --frozen-lockfile

  deploy:
    if: ${{ always() && contains(join(needs.*.result, ','), 'success') && github.ref == 'refs/heads/main' }}
    needs: [test, e2e, stage]
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: yarn --frozen-lockfile
Enter fullscreen mode Exit fullscreen mode

Como usamos um arquivo yaml, precisamos nos atentar à indentação, que é uma das bases dessa linguagem. Feita essa observação, note que temos três conjuntos principais de instruções: on, env e jobs. Destas três, apenas on e jobs são obrigatórias, a env usamos apenas as nossas validações precisam de uma variável de ambiente. No nosso caso, definimos apenas uma porta para a aplicação rodar localmente durante o deploy.

O on é utilizado para definir quais serão os gatilhos da nossa pipeline, ou seja, quando executaremos essas verificações. No nosso caso definimos que sempre quando realizarmos um push ou pull requests nas branchs main e stage.

on:
  push:
    branches:
      - main
      - stage
  pull_request:
    branches:
      - main
      - stage

env:
  PORT: 5000
Enter fullscreen mode Exit fullscreen mode

Agora vem a parte funcional da nossa pipeline. Dentro do jobs, definimos quais serão as ações executadas durante cada um dos processos. No nosso exemplo possuímos os dois primeiros jobs test e e2e, que estão configurados para rodar os testes automatizados.

Neste caso, antes de executar de fato cada etapa, definimos no runs-on e na strategy as configurações necessárias da nossa aplicação. No nosso caso, temos explicito que vamos usar um setup do Ubuntu lts e na sequência o Node na versão 16. Este ponto é específico de cada linguagem e necessita de consulta à documentação para saber qual a configuração ideal para sua aplicação.

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: yarn --frozen-lockfile

    - name: Run unit tests
      run: yarn test
Enter fullscreen mode Exit fullscreen mode

Ainda dentro dos jobs o segredo para impedir a execução da pipeline em caso de erro está no nosso if, que aparece a partir do e2e. Nesse caso, pedimos que sempre os testes tenham sucesso para somente após isso executar o próximo passo. Sem esse if, mesmo que ocorra um erro na etapa anterior, a pipeline seguirá todo o fluxo até produção, que não é nossa ideia.

stage:
    if: ${{ always() && contains(join(needs.*.result, ','), 'success') }}
    needs: [test, e2e]
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: yarn --frozen-lockfile
Enter fullscreen mode Exit fullscreen mode

Por fim, nos dois jobs finais, em stage e deploy, é quando realizamos a entrega da nossa aplicação em cada um dos ambientes. Nesse caso, usamos uma branch diferente para construir cada um desses ambientes.

Dentro desses jobs informamos os comandos para realizar o deploy da aplicação e é onde podemos integrar com outros serviços. Esse ponto é um assunto para outro momento.

https://github.com/nathyts/products-api

Top comments (0)