CI para Python com GitHub Actions

CI para Python garante que cada pull request passa por testes automáticos, verificação de tipos e lint antes de entrar no branch principal. Com pytest, ruff e mypy no GitHub Actions, você tem um pipeline robusto em menos de 50 linhas de YAML que roda em qualquer projeto Python moderno.

Workflow de CI completo para Python

Pipeline com testes, lint, tipagem e cobertura:

yaml
# .github/workflows/ci.yml
name: CI Python

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  ci:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: ['3.11', '3.12']

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'

      - name: Instalar dependências
        run: |
          pip install -r requirements.txt
          pip install -r requirements-dev.txt

      - name: Lint com ruff
        run: ruff check .

      - name: Formatação com ruff format
        run: ruff format --check .

      - name: Type check com mypy
        run: mypy app/ --ignore-missing-imports

      - name: Testes com pytest e cobertura
        run: pytest --cov=app --cov-report=xml --cov-report=term-missing

      - name: Upload cobertura
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

requirements-dev.txt para o CI

Separe dependências de desenvolvimento das de produção:

bash
# requirements.txt — produção
fastapi==0.115.0
uvicorn[standard]==0.32.0
sqlalchemy==2.0.36
asyncpg==0.30.0
pydantic-settings==2.6.0

# requirements-dev.txt — apenas desenvolvimento e CI
pytest==8.3.3
pytest-asyncio==0.24.0
pytest-cov==6.0.0
httpx==0.28.0          # cliente HTTP para testar FastAPI
ruff==0.8.0            # lint e formatter
mypy==1.13.0           # type checker
factory-boy==3.3.1     # factories para testes

# pyproject.toml — configuração centralizada
[tool.ruff]
line-length = 100
select = ["E", "F", "I", "N", "W", "UP"]
ignore = ["E501"]

[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "--strict-markers"

Testes de integração com PostgreSQL

Use services para subir PostgreSQL no CI e testar com banco real:

yaml
jobs:
  integration-tests:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready
          --health-interval 5s
          --health-retries 10

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
      - run: pip install -r requirements.txt -r requirements-dev.txt

      - name: Aplicar migrações
        env:
          DATABASE_URL: postgresql+asyncpg://testuser:testpass@localhost:5432/testdb
        run: alembic upgrade head

      - name: Testes de integração
        env:
          DATABASE_URL: postgresql+asyncpg://testuser:testpass@localhost:5432/testdb
          ENVIRONMENT: test
        run: pytest tests/integration/ -v --tb=short

Testar API FastAPI no CI

Use TestClient do httpx para testar endpoints FastAPI sem servidor externo:

python
# tests/test_api.py
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app

@pytest.fixture
async def client():
    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://test"
    ) as ac:
        yield ac

@pytest.mark.asyncio
async def test_health_check(client):
    response = await client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

@pytest.mark.asyncio
async def test_criar_usuario(client):
    response = await client.post("/users", json={
        "name": "Test User",
        "email": "test@example.com"
    })
    assert response.status_code == 201
    data = response.json()
    assert data["email"] == "test@example.com"
    assert "id" in data

Pre-commit hooks para CI local

Use pre-commit para rodar as mesmas verificações do CI antes do commit:

yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.13.0
    hooks:
      - id: mypy
        additional_dependencies: [types-all]

# Instalar e ativar:
pip install pre-commit
pre-commit install

# Rodar manualmente em todos os arquivos:
pre-commit run --all-files

# No CI — verificar que pre-commit passa:
      - name: Pre-commit checks
        uses: pre-commit/action@v3.0.1

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

ruff substituiu flake8, isort, pyupgrade e muitos outros em um único binário escrito em Rust, 10-100x mais rápido e configuração centralizada no pyproject.toml. pylint: mais verboso e completo (detecta mais problemas), mas mais lento. Recomendação 2025: ruff para lint e formatação, mypy para tipagem estática.

pytest é o padrão da comunidade Python: sintaxe mais simples (assert simples em vez de self.assertEqual), fixtures poderosas, plugins para qualquer necessidade. unittest: biblioteca padrão, sem dependências extra, mas mais verboso. Para qualquer projeto novo: pytest. Para projetos legados com unittest: podem coexistir.

Separe testes por velocidade com markers: @pytest.mark.fast e @pytest.mark.slow. No CI: rodar os rápidos em cada PR, os lentos apenas no push para main. pytest -m "not slow" para excluir os lentos. Use pytest-xdist para rodar em paralelo: pytest -n auto.

actions/setup-python com cache: pip usa o hash de requirements.txt como chave de cache. Se o hash mudar (nova dependência), o cache é invalidado e as dependências são reinstaladas. Para cache manual: use actions/cache com path: ~/.cache/pip e key baseado no hash dos arquivos de requirements.

pip audit (gratuito, mantido pela Python Packaging Authority): pip install pip-audit; pip-audit -r requirements.txt. Safety: safety check -r requirements.txt. Ambos verificam vulnerabilidades conhecidas em dependências. Adicione como step no CI: run: pip-audit -r requirements.txt --fail-on-vuln.