Tenho dedicado um tempo a me familiarizar com a forma de construir pipelines de integração contínua com o github actions e, depois de algumas tentativas de construir uma para um projeto Django + PostgreSQL, resolvi documentar.
Um primeiro arquivo do CI
O github não quer ver a gente fazendo esforço e trata logo de sugerir alguns modelos de CI muito úteis com base nos arquivos do projeto. Por exemplo, abaixo alguns modelos e o que escolhi pra configurar um primeiro workflow considerando apenas o django, com testes e linting.
Em relação ao modelo sugerido alterei apenas a última linha para adequar à estrutura de pastas do projeto e o array em python-version. O fluxo todo usa apenas a máquina principal, sem container de serviço, e ficou assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
name: Django CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: [3.7]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
run: |
python app/manage.py test app/ && flake8 --config app/.flake8
Uma breve explicação sobre cada parte que nem de longe substitui uma necessária lida na documentação que eu aconselho fortemente:
- Na seção
on
definimos os eventos que disparam o fluxo de trabalho, neste caso, em caso depush
epull request
na branchmain
. - Na seção
strategy
podemos definir uma matriz para diferentes configurações dos jobs. Aqui podemos por exemplo definir diferentes sistemas operacionais, arquiteturas e versões do python para usar no setup, mas optei por usar apenas uma (porquê? senti que deveria poupar recursos do github rs, afinal nesse primeiro momento queria primeiro fazer funcionar). Definindo dois sistemas operacionais diferentes e 3 versões do python, por exemplo, teríamos uma matriz com 6 trabalhos. Como estou usando apenas uma versão do python, na prática estou criando uma matriz de apenas 1 trabalho e não precisaria usar o strategy, mas preferi deixar a estrutura pronta para caso sinta a necessidade. - As duas primeiras etapas do build são basicamente ações prontas (
actions/checkout@v2
eactions/setup-python@v2
) que usamos no nosso job, assim apenas referenciamos e não precisamos nos preocupar com algumas etapas usuais do processo.
Abaixo o resultado do job, executando etapa de teste unitário e linting com flake8.
Depois de atualizar o banco de dados do projeto para o PostgreSQL, precisei pesquisar como fazer isso no github actions.
Adicionando um container de serviço
Com o SQLite, apenas a própria máquina principal com o projeto django é suficiente para rodar os testes. Adicionando um banco como PostgreSQL ou MySQL, precisamos criar um container de serviço, semelhante ao que é feito com o docker-compose.
Variáveis de ambiente
Vamos usa um container e a máquina runner (a do projeto django) precisa saber como acessar o banco de dados no container; vamos fazer isso usando variáveis de ambiente. É possível definir variáveis de ambiente no contexto env
que serão utilizados nos jobs em três níveis do fluxo (com base na própria documentação):
- Todo fluxo de trabalho, usando
env
no nível superior do arquivo. - O conteúdo de um job em um fluxo de trabalho, usando
jobs.<job_id>.env
; - Uma etapa específica dentro de um job, usando
jobs.<job_id>.steps[*].env
.
Isso é muito importante e ajuda especialmente nessas integrações. Então, defini variáveis de configuração do banco de dados no segundo nível, no escopo do único job do fluxo - elas portanto ficam disponíveis em todo o job, incluindo nas etapas finais em que são executadas as migrações e os testes. Como o nome das variáveis de ambiente utilizadas pela imagem do postgres são diferentes, referenciei com ${{ env.<VAR> }}
. Isso já resolve um eventual problema de credenciais inválidas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
name: Django CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
env:
DB_HOST: localhost
DB_NAME: postgres
DB_USER: postgres
DB_PASSWORD: supersecretpassword
services:
postgres:
image: postgres:10-alpine
env:
POSTGRES_DB: ${{ env.DB_NAME }}
POSTGRES_USER: ${{ env.DB_USER }}
POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }}
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
strategy:
max-parallel: 4
matrix:
python-version: [3.7]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install postgres prerequisites
run: sudo apt-get install libpq-dev
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run migrations
run: python app/manage.py wait_for_db && python app/manage.py migrate
- name: Run Tests
run: |
flake8 --config app/.flake8 && python app/manage.py test app/
Há muito mais sobre variáveis de ambiente no github actions que você pode querer saber e a documentação é excelente.
Abaixo uma explicação mais pormenorizada sobre o container de serviço e as modificações na máquina runner.
Container de serviço
Citando a documentação:
Os containers de serviço são containers do Docker que fornecem uma maneira simples e portátil de hospedar serviços que você pode precisar para testar ou operar seu aplicativo em um fluxo de trabalho. Por exemplo, seu fluxo de trabalho pode precisar executar testes de integração que exijam acesso a um banco de dados e cache de memória.
No meu caso, tinha uma máquina runner com o projeto django e precisava de apenas um container de serviço para o PostgreSQL. A documentação é vasta e inclui um excelente guia para nos ajudar no processo. Minha imagem ficou configurada assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# jobs.build
services:
postgres:
image: postgres:10-alpine
env:
POSTGRES_DB: ${{ env.DB_NAME }}
POSTGRES_USER: ${{ env.DB_USER }}
POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }}
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
As variáveis de ambiente definidas para o job foram acessadas e seus valores foram usados para definição das variáveis de ambiente esperadas pela imagem postgres:10-alpine
, conforme já explicado. Em options
podemos usar um recurso para aguardar até o postgres estar pronto. Precisamos também mapear a porta do container de serviço para o host porque, por padrão, o container de serviço não expõe a porta - nada diferente do esperado.
Requisitos para a máquina runner
Um último e importante ajuste é o de garantir que a máquina runner tenha o que é preciso para usar o PostgreSQL. Precisei instalar as dependências necessárias para o pacote psycopg2
. Além disso, no CI anterior não tinha a etapa que executava as migrações, então adicionei. Tem-se então as etapas Install postgres prerequisites
e Run migrations
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# em jobs.build
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install postgres prerequisites
run: sudo apt-get install libpq-dev
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run migrations
run: python app/manage.py wait_for_db && python app/manage.py migrate
- name: Run Tests
run: |
flake8 --config app/.flake8 && python app/manage.py test app/
O resultado (após, claro, um frutífero processo de falha → mais uma lida na documentação → ajuste → nova tentativa).
É isso.