Artigo

CI/CD com GitHub Actions para Projetos Python de IA

CI/CD com GitHub Actions para Projetos Python de IA
Tempo de leitura8 minutos

Projeto de IA sem automação vira um emaranhado de “funciona na minha máquina”. Um pipeline de CI/CD com GitHub Actions garante que cada mudança passe por lint, testes e build antes de chegar em produção — e que o deploy seja um clique (ou nenhum). Neste guia mostro como montar isso do zero para um projeto Python de IA.

Vou usar um caso comum: uma API FastAPI que carrega um modelo e responde inferências, empacotada em Docker.

O que é CI/CD e por que importa em IA

  • CI (integração contínua): cada push roda lint, testes e validações automaticamente.
  • CD (entrega contínua): o que passa nos testes é empacotado e publicado sem trabalho manual.

Em IA, isso é ainda mais valioso: dependências pesadas, código sensível a versão e modelos que precisam ser validados antes de ir ao ar.

Estrutura mínima do projeto

projeto/
  app/main.py
  tests/test_api.py
  requirements.txt
  pyproject.toml
  Dockerfile
  .github/workflows/ci.yml

O primeiro workflow: lint e testes

Comece simples. Este workflow roda em cada push e pull request:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  testes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -r requirements.txt
      - run: pip install ruff pytest
      - run: ruff check .
      - run: pytest -q

O cache do pip acelera muito a instalação. Sem ele, cada execução reinstala tudo.

Matriz de versões do Python

Se você publica uma biblioteca, teste em várias versões de uma vez:

    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    steps:
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

Testando sem gastar tokens de IA

Nunca chame a API real do modelo nos testes. Use mocks. Isso deixa a suíte rápida, gratuita e determinística.

from unittest.mock import patch

def test_resumir(client):
    with patch("app.main.chamar_llm", return_value=("resumo", 10)):
        r = client.post("/api/resumir", json={"texto": "conteudo"})
    assert r.status_code == 200
    assert r.json()["resumo"] == "resumo"

Build da imagem Docker no pipeline

Depois dos testes, construa a imagem. Use o cache de camadas do GitHub Actions para acelerar builds.

  build:
    needs: testes
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          tags: minha-api-ia:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Publicando a imagem no registry

Para entregar de verdade, publique no GitHub Container Registry (GHCR):

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

Usar o SHA do commit como tag torna o deploy rastreável: você sempre sabe qual código está no ar.

Segredos e ambientes

Chaves de API ficam em Secrets do repositório, nunca no YAML.

      - run: ./deploy.sh
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

Para deploy com aprovação manual, use Environments: você define revisores e o job só roda depois do aval.

Deploy automatizado

O deploy varia conforme o alvo. Um exemplo via SSH para um servidor:

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
            docker compose up -d --no-deps api

Qualidade de código com pre-commit

Rode o lint antes mesmo de chegar ao CI. O pre-commit evita commits problemáticos.

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.0
    hooks:
      - id: ruff

Erros comuns

  • Chamar a API do modelo nos testes (lento, caro, instável).
  • Colocar segredos direto no workflow.
  • Não fixar versões de dependências — builds quebram sozinhos.
  • Deploy sem aprovação em produção.
  • Ignorar cache — pipelines lentos desmotivam o time.

Checklist de pipeline maduro

  1. Lint + formatação automáticos.
  2. Testes rápidos com mocks.
  3. Build de imagem com cache.
  4. Publicação com tag por commit.
  5. Deploy com ambiente protegido.
  6. Segredos fora do código.

Rodando o pipeline localmente antes de subir

Antes de confiar no CI, rode os mesmos passos na sua máquina. O act permite executar workflows do GitHub Actions localmente, o que acelera muito o ciclo de ajuste.

act -j testes

Se o pipeline passa local, é muito provável que passe no GitHub — e você evita o ciclo lento de commit, espera, erro e commit.

Estratégia de branches e proteção

O CI só protege de verdade se o merge for bloqueado quando os testes falham. Ative a proteção de branch na main: exija que o workflow passe antes de permitir o merge, e exija revisão de código. Isso transforma o pipeline em um portão real, não em um aviso ignorado.

Versionamento e releases automáticas

Quando você publica uma biblioteca, pode gerar tags e changelog automaticamente a partir dos commits. Ferramentas como o semantic-release analisam as mensagens no padrão Conventional Commits e decidem a próxima versão.

# commits no padrao
git commit -m "feat: adiciona endpoint de resumo"
git commit -m "fix: corrige timeout do provedor"

Com isso, a versão sobe sozinha e o changelog é escrito sem esforço manual.

Cache inteligente de dependências

O cache do pip é o que mais acelera o CI de Python. Além dele, cacheie também os navegadores do Playwright, se usar, e os modelos baixados em testes. Um pipeline que antes levava 8 minutos pode cair para 2.

      - uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: pip-${{ hashFiles('requirements.txt') }}

Monitorando o custo do CI

Pipelines longos consomem minutos do plano. Fique de olho: rodar a matriz completa em todo push pode ser desperdício. Uma boa prática é rodar o conjunto completo só em pull requests e na main, deixando pushes de feature com uma suíte reduzida.

Testes de performance no pipeline

Além de corretude, meça performance. Um teste simples de latência evita que uma mudança deixe a API lenta sem ninguém perceber.

import time

def test_latencia_aceitavel():
    inicio = time.perf_counter()
    processar("texto de teste")
    duracao = time.perf_counter() - inicio
    assert duracao < 0.5  # segundos, com mock

Para benchmarks mais sérios, use pytest-benchmark e acompanhe a evolução ao longo do tempo.

Matriz de compatibilidade de dependências

Projetos de IA quebram quando uma dependência sobe de versão sozinha. Um job semanal que reinstala tudo do zero detecta isso antes de virar problema.

on:
  schedule:
    - cron: "0 6 * * 1"   # toda segunda
jobs:
  compatibilidade:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -U -r requirements.txt
      - run: pytest -q

Se esse job falhar, você sabe que algo upstream mudou — antes que o cliente reclame.

Documentando o pipeline

Um badge de status no README mostra de imediato se o build está verde. Isso cria confiança e ajuda a detectar regressões visualmente.

![CI](https://github.com/usuario/repo/actions/workflows/ci.yml/badge.svg)

Deploy blue-green com GitHub Actions

Para reduzir risco, mantenha duas versões no servidor e troque o tráfego só depois que a nova passar no healthcheck. Se algo falhar, o rollback é imediato.

          script: |
            docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
            docker tag ghcr.io/${{ github.repository }}:${{ github.sha }} app:novo
            docker compose up -d --no-deps --scale api=2 api
            sleep 10
            curl -f http://localhost:8000/health || (docker compose up -d --no-deps api && exit 1)

Notificações de falha

Pipeline que falha em silêncio é pipeline inútil. Notifique o time quando algo quebrar — no Slack, no Telegram ou por e-mail.

      - name: Avisar no Telegram
        if: failure()
        run: |
          curl -s -X POST "https://api.telegram.org/bot${{ secrets.TG_TOKEN }}/sendMessage" \
            -d chat_id=${{ secrets.TG_CHAT }} \
            -d text="CI falhou: ${{ github.repository }} (${{ github.sha }})"

Dependabot e atualizações automáticas

Manter dependências atualizadas reduz vulnerabilidades. O Dependabot abre pull requests automáticos quando surge uma versão nova, e o seu pipeline valida cada uma antes do merge.

version: 2
updates:
  - package-ecosystem: pip
    directory: "/"
    schedule:
      interval: weekly
  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: monthly

Conclusão e próximos passos

Um pipeline maduro evolui por etapas: primeiro lint e testes, depois build e publicação, por último deploy com aprovação. Não tente montar tudo de uma vez — adicione uma camada por vez e valide o ganho.

Segredos: OIDC em vez de chaves de longa duração

Guardar chaves de nuvem como segredos é arriscado. Uma alternativa moderna é o OIDC: o GitHub Actions assume uma identidade temporária na nuvem, sem chave estática. Isso elimina um vetor de vazamento.

    permissions:
      id-token: write
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123:role/deploy
          aws-region: sa-east-1

Limites de tempo e concorrência

Sem controle, um workflow pode ficar preso por horas e consumir minutos do plano. Defina timeout-minutes e agrupe execuções por branch para cancelar as antigas.

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  testes:
    timeout-minutes: 15

Resumo

CI/CD não é luxo de empresa grande. Com algumas dezenas de linhas de YAML você garante que toda mudança passe por lint, testes e build antes de ir ao ar — e que o deploy seja reproduzível. Comece pelo workflow de testes, adicione build e só depois automatize o deploy. O ganho de confiança é imediato.

Anterior e próximo

Leituras recentes

Temas parecidos para continuar navegando no blog.

Conteúdo criado para leitura limpa, boa hierarquia visual e navegação clara.