CI/CD para Python com GitHub Actions

Para configurar CI em um projeto Python, crie .github/workflows/ci.yml, instale o projeto com suas dependências de desenvolvimento e execute Ruff, mypy, pytest com cobertura e o build. O workflow funcional deste guia testa Python 3.11, 3.12 e 3.13, usa cache do pip, limita permissões e salva o pacote gerado como artifact. Você pode copiá-lo e ajustar os comandos ao seu projeto.

O resultado é um controle automático antes do merge: cada push ou pull request precisa provar que estilo, tipos, testes e empacotamento continuam válidos. Publicar em produção é uma etapa separada e opcional.

O que são CI, entrega contínua e deploy contínuo?

Integração contínua (CI) é a prática de integrar mudanças pequenas com frequência e validá-las automaticamente. Em Python, isso costuma envolver lint, testes, cobertura, análise de tipos e criação do pacote. A CI reduz a chance de um problema ficar escondido até o fim de uma entrega.

O termo CD pode representar duas práticas diferentes:

  • Entrega contínua (continuous delivery): todo código aprovado fica em estado publicável, mas uma pessoa ou aprovação de ambiente decide quando liberar.
  • Deploy contínuo (continuous deployment): toda mudança que passa pelos controles é publicada automaticamente.

Portanto, CI/CD não obriga deploy automático. Uma equipe pode adotar CI e entrega contínua, manter uma aprovação manual para produção e ainda ter um processo maduro. Para aplicações empacotadas em containers, o guia de Docker com Python complementa a etapa de entrega. Para bibliotecas, veja como publicar um pacote Python no PyPI.

Estrutura de .github/workflows

O GitHub procura arquivos YAML em .github/workflows/. Cada arquivo é um workflow; ele define eventos em on, permissões, variáveis e um ou mais jobs. Cada job roda em um runner e contém etapas (steps). Consulte a documentação oficial do GitHub Actions para eventos e sintaxe.

Uma estrutura pequena e legível é suficiente:

meu-projeto/
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── ci.yml
├── src/meu_pacote/
├── tests/
├── pyproject.toml
└── README.md

Evite colocar toda automação em um arquivo gigantesco. CI, publicação e deploy têm gatilhos e permissões diferentes; separar ci.yml de release.yml diminui o impacto de um erro e torna cada fluxo mais fácil de revisar.

Dependências consistentes no pyproject.toml

O runner deve instalar exatamente as ferramentas chamadas no workflow. Este recorte usa uma extra dev, adequada tanto à máquina local quanto à CI:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "meu-pacote"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []

[project.optional-dependencies]
dev = [
  "build>=1.2,<2",
  "mypy>=1.10,<2",
  "pytest>=8,<9",
  "pytest-cov>=5,<7",
  "ruff>=0.5,<1",
]

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

[tool.coverage.run]
source = ["meu_pacote"]

[tool.coverage.report]
fail_under = 90
show_missing = true

[tool.mypy]
python_version = "3.11"
strict = true

[tool.ruff]
target-version = "py311"
line-length = 100

Limites de versão equilibram reprodutibilidade e atualização. Em aplicações, um lockfile pode ser mais apropriado; entenda as opções no guia de gerenciamento de dependências com pip. Mantenha local e CI no mesmo comando para não criar o clássico “funciona na minha máquina”.

Workflow completo para Python

Crie .github/workflows/ci.yml:

name: CI

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

permissions:
  contents: read

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

jobs:
  quality:
    name: Python ${{ matrix.python-version }}
    runs-on: ubuntu-latest
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.11", "3.12", "3.13"]

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
          cache-dependency-path: pyproject.toml

      - name: Install project
        run: |
          python -m pip install --upgrade pip
          python -m pip install -e ".[dev]"

      - name: Ruff
        run: |
          ruff check .
          ruff format --check .

      - name: Mypy
        run: mypy src

      - name: Tests and coverage
        run: pytest --cov=meu_pacote --cov-report=term-missing --cov-report=xml

      - name: Build distributions
        if: matrix.python-version == '3.13'
        run: python -m build

      - name: Upload distributions
        if: matrix.python-version == '3.13'
        uses: actions/upload-artifact@v4
        with:
          name: python-distributions
          path: dist/
          if-no-files-found: error
          retention-days: 7

checkout@v4 baixa o código e setup-python@v5 instala cada interpretador. O cache integrado usa os arquivos indicados para gerar sua chave; ele guarda downloads do pip, não o ambiente virtual inteiro. fail-fast: false deixa todas as versões terminarem, revelando se a falha é específica de um interpretador.

Ruff verifica regras e formatação conforme sua documentação oficial. Isso automatiza convenções abordadas no guia da PEP 8 para Python. O mypy valida os contratos de tipos; consulte a documentação do mypy e nosso artigo sobre type hints em Python.

O pytest gera saída no terminal e coverage.xml, enquanto fail_under = 90 transforma cobertura abaixo do limite em falha. Cobertura mede linhas executadas, não qualidade; escreva cenários úteis com o guia de testes automatizados com pytest. O build cria wheel e source distribution apenas uma vez, e o artifact permite baixar exatamente esses arquivos pela execução do Actions.

Permissões mínimas, secrets e OIDC

permissions: contents: read segue o princípio do menor privilégio. Não conceda write-all por conveniência. Um workflow que apenas testa não precisa escrever no repositório; um fluxo de release deve receber somente permissões necessárias, preferencialmente no job específico.

Use GitHub Secrets para tokens que realmente precisam ser armazenados e nunca imprima um secret, passe-o por argumento visível ou execute código não confiável com credenciais. Dependabot e pull requests de forks também têm restrições de secrets, uma proteção importante contra exfiltração.

Para AWS, Azure, Google Cloud e serviços compatíveis, prefira OpenID Connect (OIDC). O job recebe id-token: write, troca sua identidade do GitHub por uma credencial curta no provedor e elimina chaves permanentes. Restrinja a confiança por repositório, branch, tag ou environment e siga o guia oficial de segurança de deployments com OIDC. Essa permissão deve existir apenas no job de deploy, não na CI acima.

Environments permitem revisores obrigatórios, regras de branch e secrets por ambiente. Assim, entrega contínua pode produzir um artifact e um job posterior pode aguardar aprovação para produção. Deploy contínuo é opcional: adote-o somente quando testes, observabilidade e rollback sustentarem essa decisão.

Branch protection e checks obrigatórios

O workflow só protege a main se as regras do repositório impedirem atalhos. Em Settings > Rules > Rulesets (ou regras de proteção de branch), exija pull request, aprovação e o check Python 3.11, Python 3.12 e Python 3.13, ou um job agregador estável. Bloqueie merge enquanto a conversa estiver pendente e limite bypass a casos administrados.

Se o nome de um job obrigatório mudar, atualize a regra; caso contrário, o merge pode ficar esperando um check que nunca chegará. Código limpo continua responsabilidade humana: use a automação junto das práticas de Clean Code em Python.

Dependabot e badge da CI

Configure .github/dependabot.yml para receber atualizações das actions e do ecossistema pip:

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

Revise changelogs e deixe a CI avaliar cada atualização; não faça merge automático sem controles. Para exibir o estado no README.md, troque proprietário e repositório:

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

O badge comunica o último resultado do fluxo, não certifica segurança nem qualidade absoluta.

Otimização sem fragilizar o pipeline

Comece simples e meça a duração das etapas antes de otimizar. O cache do setup-python já evita downloads repetidos. Defina cache-dependency-path para o manifesto ou lockfile correto e lembre que caches são descartáveis: uma execução precisa continuar correta após um cache miss.

concurrency cancela execuções antigas da mesma branch, economizando runners quando novos commits chegam rapidamente. Use timeout-minutes para conter processos travados. Execute o build em uma versão, mas mantenha os testes na matriz inteira. Em projetos grandes, lint e tipos podem ir para um job sem matriz, desde que testes continuem cobrindo todos os Pythons suportados.

Não faça cache de .venv sem necessidade: caminhos, binários e versões tornam esse cache frágil. Não misture otimização com ocultação de falhas, usando continue-on-error em um controle obrigatório.

Erros comuns

  • YAML no diretório errado: somente arquivos em .github/workflows/ são workflows.
  • Dependência ausente: chamar ruff ou mypy sem incluí-los na extra instalada causa command not found.
  • Pacote não importável: prefira instalar o projeto com pip install -e ".[dev]" em vez de manipular PYTHONPATH.
  • Cache desatualizado: inclua o arquivo que realmente governa dependências em cache-dependency-path.
  • Cobertura enganosa: um percentual alto pode não cobrir casos-limite, erros ou comportamento do usuário.
  • Versões não suportadas na matriz: a matriz deve corresponder a requires-python e à política documentada.
  • Permissões amplas: conceder escrita ou expor secrets a código de pull request aumenta o impacto de dependências comprometidas.
  • Deploy no mesmo job: testes e produção acabam compartilhando credenciais, gatilhos e risco desnecessários.

Checklist para colocar CI/CD em produção

  • [ ] pyproject.toml declara versões de Python e a extra dev.
  • [ ] Ruff, mypy, pytest e build rodam localmente com os mesmos comandos.
  • [ ] A matriz cobre todas as versões oficialmente suportadas.
  • [ ] Cobertura tem um limite útil e testes exercitam comportamento, não apenas linhas.
  • [ ] O workflow possui contents: read, timeout e concorrência configurados.
  • [ ] O pacote gerado é salvo como artifact e tem retenção definida.
  • [ ] Checks obrigatórios e revisão estão ativos para a branch principal.
  • [ ] Dependabot atualiza pip e GitHub Actions em pull requests revisáveis.
  • [ ] Secrets são mínimos; deploy em nuvem usa OIDC quando disponível.
  • [ ] Publicação e deploy ficam em workflow separado, com environment e aprovação quando necessário.

Com essa base, a CI oferece feedback rápido e reproduzível, a entrega gera um pacote rastreável e o deploy permanece uma escolha consciente. O melhor pipeline não é o que acumula etapas: é o que bloqueia defeitos relevantes, preserva segurança e continua compreensível para toda a equipe.