GitLab CI – Integração Contínua sem sair do repositório

GitLab CI – Integração Contínua sem sair do repositório

Ferramentas de CI/CD hoje em dia andam de mãos dadas com os nossos projetos – dificilmente vemos um repositório no GitHub, GitLab, Bitbucket, Gitea… sem alguma configuração de integração contínua. Todos estes têm a opção de configurar webhooks, que são basicamente ações eventuais: se repositório recebeu push, então fazer uma requisição HTTP em uma URL configurada com os dados deste push, do repositório, etc. Este é o caminho padrão para quem usa ferramentas de CI standalone como o Jenkins🇺🇸. Além disso, serviços diferentes tomam decisões diferentes: o Github, por exemplo, não tem uma ferramenta própria de CI/CD. É muito comum ver arquivos de configuração das ferramentas Travis🇺🇸 e CircleCI🇺🇸 em seus repositórios.

E então temos Bitbucket e GitLab. Estes dois decidiram seguir um outro caminho: eles têm a sua própria ferramenta de CI/CD, que é chamada internamente de Pipelines. A configuração não chega a ser nada de outro mundo: é só mais um punhado de valores YAML que temos que decorar e usar.

A 4Linux internamente usa o GitLab, então faremos os nossos exemplos em um repositório na instância oficial: https://gitlab.com🇺🇸. O autor presume que você possui um conhecimento pelo menos básico de git e repositórios.

O Projeto

Faremos um projeto simples – até de mais – para demonstrar como usar o Pipelines do GitLab. O projeto será basicamente 4 arquivos de código-fonte Python: 2 de aplicação e 2 de testes. Mandaremos o GitLab executar os testes para nós.

Nosso primeiro arquivo é um em que implementamos duas operações: soma e subtração de dois números, ops.py:

def add(x: int, y: int) -> int:
    return x + y

def sub(x: int, y: int) -> int:
    return x - y

Nada de outro mundo. A seguir temos o arquivo onde testamos as nossas operações, test_ops.py:

import unittest
import ops

class TestOps(unittest.TestCase):
    def test_add(self):
        for x in range(1, 6):
            y = x + 1
            with self.subTest(x=x, y=y, **{'x+y': x+y}):
                self.assertEqual(ops.add(x, y), x + y)

    def test_sub(self):
        for x in range(14, 9, -1):
            y = x - 5
            with self.subTest(x=x, y=y, **{'x-y': x-y}):
                self.assertEqual(ops.sub(x, y), x - y)

Ao executar os testes, vemos a saída esperada:

$ python -m unittest -v test_ops
test_add (test_ops.TestOps) ... ok
test_sub (test_ops.TestOps) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Agora que estamos com os nossos testes funcionando, já podemos começar a escrever o nosso arquivo de pipeline. Criamos então um novo arquivo chamado .gitlab-ci.yml na raíz do projeto e escrevemos o seguinte nele:

stages:
  - test

test-ops:
  stage: test
  image: python:slim-bullseye
  script:
    - python -m unittest

Seguido de um commit e um push.

A primeira execução do Pipeline

Assim que o GitLab identificar que há um arquivo .gitlab-ci.yml na raíz do projeto, ele deve automaticamente habilitar o fluxo de pipelines do repositório. É possível ver o processo ao navegar até a seção de pipelines do projeto (imagem 1) ou acessar diretamente pela mensagem do último commit na página inicial, onde deve haver um 🕒 relógio azul caso ainda esteja sendo executado, um ✅ checkmark verde caso tenha finalizado com sucesso, ou um ❌ X vermelho caso tenha havido algum problema (imagem 2).

imagem 1:
navegação até a página de pipelines pela barra lateral

imagem 2:
navegação até o pipeline do commit em questão

Na página seguir vemos todos os pipelines executados; deve haver apenas um neste instante. Perto da mensagem do commit, há um número referente ao pipeline, o seu identificador (imagem 3). Podemos clicar neste identificador para ver os jobs do pipeline que, agora, consiste de apenas um: test-ops (imagem 4). Precisamos apenas clicar neste job para ver os detalhes de sua execução em uma janela parecida com um terminal (imagem 5).

imagem 3:
id do pipeline

imagem 4:
jobs do pipeline selecionado

imagem 5:
saída do terminal de um job

Explicando o nosso .gitlab-ci.yml

Vamos numerar cada linha do nosso arquivo:

stages: # --------------------- 1
  - test # -------------------- 2

test-ops: # ------------------- 3
  stage: test # --------------- 4
  image: python:slim-bullseye # 5
  script: # ------------------- 6
    - python -m unittest # ---- 7
  1. Aqui definimos o objeto stages, que é responsável por listar todos os estágios do nosso pipeline. Podemos colocar quantos stages forem necessários, e os seus nomes podem ser arbitrários. test é um nome comum de estágio de pipeline. Os estágios são executados em ordem de definição, e jobs que pertencem a um mesmo stage são executados em paralelo.

  2. Aqui definimos o nosso primeiro (e único, a princípio) estágio: test. Usamos este nome no momento de definição de um job para indicar quando que este deve ser executado.

  3. Este é o nosso job. O job é o que define quais instruções queremos executar neste instante do pipeline. Seu nome também é arbitrário; eu escolhi test-ops, mas poderia ser qualquer outro nome. Na imagem 4 podemos ver um objeto circulado com o nome test-ops em baixo do nome de seu stage, Test.

  4. Aqui é onde definido a qual stage este job pertence. No caso, test. A única restrição que temos que seguir é que o nome do stage precisa ser um stage válido listado no nosso objeto stages do item 1.

  5. Neste ponto definimos qual imagem do docker usaremos para executar este job. O GitLab cuida de baixar a imagem, que deve existir no Docker Hub ou seja qual for o repositório de imagens utilizado.

  6. No ponto 6 iniciamos o nosso objeto script, que consiste de uma lista de steps (passos). Cada step deve ser um comando shell válido.

  7. Finalmente, aqui definimos o nosso único step: python -m unittest. Este é o comando que usamos para executar os testes automatizados, que pode ser visto na imagem 5 acima.

Incrementando o nosso projeto

Vamos adicionar um app simples com Flask para poder testar nossa rota. O app consistirá de uma única rota que somará dois números. Criaremos, então, nosso app.py e escreveremos o seguinte:

import flask

app = flask.Flask(__name__)

@app.route("/add/<int:x>/<int:y>")
def add(x: int, y: int):
    return str(x - y)

Seguido do nosso test_app.py:

import os
import unittest
from flask.testing import FlaskClient
from app import app

class TestApp(unittest.TestCase):
    @unittest.skipUnless(os.getenv("should_test"), "test only when required")
    def test_add(self):
        x, y = 2, 3

        with FlaskClient(app) as client:
            res = client.get(f"/add/{x}/{y}")
            self.assertEqual(res.data.decode(), str(x+y))

Existem dois pontos importantes no que escrevemos aqui:

  • Estamos usando uma biblioteca de terceiros, Flask;
  • O nosso teste só vai ser executado quando a variável de ambiente should_test estiver definida.

Então se tentarmos testar normalmente, o Python nos mostrará um erro dizendo que não encontra a biblioteca (partes omitidas para simplicidade):

$ python -m unittest -v test_app
test_app (unittest.loader._FailedTest) ... ERROR
ERROR: test_app (unittest.loader._FailedTest)
...
ModuleNotFoundError: No module named 'flask'
Ran 1 test in 0.000s
FAILED (errors=1)

Para que funcione, precisamos instalar a biblioteca. O faremos em um ambiente virtual:

$ python -m venv venv
$ . venv/bin/activate
$ pip install flask
$ python -m unittest -v test_app

E a sua saída:

$ python -m unittest -v test_app
test_add (test_app.TestApp) ... skipped 'test only when required'

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK (skipped=1)

Como esperado, o teste não executa a menos que a variável de ambiente esteja configurada.

$ should_test=1 python -m unittest -v test_app
test_add (test_app.TestApp) ... FAIL
AssertionError: '-1' != '5'
- -1
+ 5
Ran 1 test in 0.088s
FAILED (failures=1)

E essa é a nossa saída esperada, pois fizemos uma subtração em vez de uma adição no código da nossa rota. Deixaremos assim para ver como aparece no GitLab. Agora modificaremos o nosso .gitlab-ci.yml para que haja uma seção para o app:

stages:
  - test

default:
  image: python:slim-bullseye

test-ops:
  stage: test
  script:
    - python -m unittest -v test_ops

test-app:
  stage: test
  variables:
    should_test: "true"
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
  before_script:
    - python -m venv venv
    - . venv/bin/activate
    - pip install Flask==2.1.1
  script:
    - python -m unittest -v test_app

Nós:

  • adicionamos um objeto default, que possui uma imagem docker como único atributo;
  • removemos o atributo image do nosso job test-ops;
    • pois com o objeto default, definimos uma imagem para todos os jobs.
  • alteramos o script do job test-ops para testar apenas o ops.py;
  • criamos o job test-app, que é parecido com o test-ops mas com propriedades adicionais:
    • definimos um objeto variables com a variável de ambiente should_test;
    • definimos um objeto rules, que determina quando este job deve ser executado:
      • apenas quando o contexto do pipeline for um Merge Request;
    • definimos um objeto before_script, que diz à ferramenta o que deve ser feito antes de executarmos os passos do script:
      • criamos e ativamos um ambiente virtual, e
      • instalamos Flask nele.

Depois disso, podemos criar um novo branch e realizar o commit e push nele:

$ git checkout -b feature/add-app
$ git add -A
$ git commit -m "add app"
$ git push -u origin feature/add-app

Agora vá à página do seu repositório e abra um Merge Request para o seu branch principal. De volta à página de Pipelines você deve ver o seguinte:

imagem 6:
pipelines bem e mal sucedidos

Dois pipelines executados! Isso é por causa da regra que demos ao test-app. O job do test-ops é executado normalmente quando fazemos o push, mas o job do test-app só é acionado e executado quando estamos no contexto de um merge request. Perceba que o commit é o mesmo. Só muda mesmo o contexto.

A mensagem de erro do job mal-sucedido:

imagem 7:
mensagem de erro de job falhado

O mesmo erro que nós vimos acima, em nossa execução local.

Antes de corrigirmos o nosso app, vamos adicionar uma seção de clean up no nosso .gitlab-ci.yml.

stages:
  - test
  - clean-up

# algumas seções omitidas para simplicidade
default:
  ...

test-ops:
  ...

test-app:
  ...

clean-up:
  stage: clean-up
  rules:
    - when: always
  script:
    - echo "cleaning up..."
  • Criamos um novo stage, clean-up;
  • Criamos um novo job, clean-up;
  • Associamos os dois pela propriedade stage do job;
  • Por meio do objeto rules, definimos que este job deve sempre (always) ser executado;
  • Adicionamos um script simples, que apenas mostra um texto na tela.

Agora realizaremos o commit seguido do push e veremos o resultado na página de pipelines do projeto.

$ git add -A
$ git commit -m "add clean up stage and job"
$ git push

E, depois de executados os jobs, vemos o seguinte resultado:

imagem 8:
pipelines com 2 stages

Podemos ver que, agora, há o novo estágio presente e sendo executado. Normalmente, um pipeline interromperia sua execução no instante em que um stage falha. Mas, como dissemos à ferramenta que o clean-up deve ser executado sempre, o pipeline segue em frente até o final. Faça o teste: comente ou remova a regra que definimos para o job e commite-pushe novamente.
Vejamos a saída do terminal deste stage:

imagem 9:
saída do stage=clean-up job=clean-up

Agora só nos falta corrigir o erro que inserimos no nosso app.py para que o pipeline seja consertado:

def add(x: int, y: int):
-   return str(x - y)
+   return str(x + y)
$ git add -A
$ git commit -m "fix app error"
$ git push

Fazemos o push e aceitamos o merge request para que possamos ver os últimos 3 pipelines executados:

imagem 10:
resultados dos últimos três pipelines

E assim finalizamos o nosso primeiro projeto utilizando a ferramenta de CI/CD do GitLab!

Finalizando

O projeto que fizemos durante este post foi bem simples, e mesmo assim deu pra escrever bastante e apresentar algumas funcionalidades interessantes da ferramenta. Porém não acaba aqui; esta é só a ponta do iceberg. Existem muitas, muitas outras funcionalidades legais e interessantes disponíveis e prontas para uso. Se for do seu interesse saber mais, o melhor lugar é a referência oficial🇺🇸 deles. As seções sobre variáveis🇺🇸 e palaras chave do arquivo YAML🇺🇸 são especialmente relevantes.

E aí, quais processos vocês estão prontos para automatizar com essa ferramenta incrível?

Líder em Treinamento e serviços de Consultoria, Suporte e Implantação para o mundo open source. Conheça nossas soluções:

CURSOSCONSULTORIA

Anterior Invista na sua carreira com 50% de desconto em cursos de TI
Próxima Domine o Linux: Torne-se um Administrador de Sistemas Linux

About author

Você pode gostar também

Desenvolvimento

Curso Python Fundamentals: Sua porta de entrada para o mundo Python

Com foco em atrair diversos perfis e atender à uma demanda crescente por profissionais que conheçam a  linguagem, o curso foi remodelado para ser a sua porta de entrada para

DevOps

DevSecOps: 6 passos para implementar segurança no desenvolvimento de software

A cultura DevOps se tornou um marco na história da tecnologia, tanto que muitos recrutadores começaram a utilizar este nome até para descrição de vagas. Mas onde fica a segurança

Desenvolvimento

Integração de Chat Funcional com Ferramenta Rocket.Chat: Passo a Passo

Dando continuidade o nosso post anterior onde entendemos como utilizar as APIs do Rocket.Chat para manipular o Omnichannel, agora iremos integrar um chat funcional com essa ferramenta. Irei partir do