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:
[](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
ruffoumypysem incluí-los na extra instalada causacommand not found. - Pacote não importável: prefira instalar o projeto com
pip install -e ".[dev]"em vez de manipularPYTHONPATH. - 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-pythone à 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.tomldeclara versões de Python e a extradev. - [ ] 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.