Artigo 45 — CI/CD com GitHub Actions
Prof. Ricardo Matos Módulo 8 · Testes, Qualidade e Boas Práticas · Artigo 45 de 52
Introdução
CI/CD — Integração Contínua e Entrega Contínua — é a prática de automatizar a verificação, teste e entrega de software a cada mudança no código. Sem CI/CD, testes são executados manualmente, deploys são processos manuais propensos a erros e bugs chegam à produção sem serem detectados. GitHub Actions é a plataforma nativa do GitHub para CI/CD — gratuita para projetos públicos, integrada ao repositório e com um ecossistema imenso de ações reutilizáveis.
Conceitos Fundamentais
Developer GitHub Servidor
│ │ │
│ git push ──► │ │
│ │ │
│ Trigger Event │
│ │ │
│ ┌────▼────┐ │
│ │ Workflow │ │
│ │ CI/CD │ │
│ └────┬────┘ │
│ │ │
│ ┌────▼────────────────┐ │
│ │ Job: test │ │
│ │ - lint │ │
│ │ - type check │ │
│ │ - pytest │ │
│ │ - coverage │ │
│ └────┬────────────────┘ │
│ │ │
│ ┌────▼────────────────┐ │
│ │ Job: build │ │
│ │ - docker build │ │
│ │ - docker push │ │
│ └────┬────────────────┘ │
│ │ │
│ ┌────▼────────────────┐ │
│ │ Job: deploy │ │──► Deploy
│ │ - deploy staging │ │
│ │ - smoke tests │ │
│ │ - deploy prod │ │
│ └─────────────────────┘ │
Estrutura de um Workflow
.github/
└── workflows/
├── ci.yml ← testes e qualidade
├── cd.yml ← deploy
├── release.yml ← publicação de releases
└── schedule.yml ← tarefas agendadas
Um workflow é um arquivo YAML com: - Triggers — quando ele é executado - Jobs — grupos de steps que rodam em paralelo ou sequência - Steps — comandos individuais ou actions reutilizáveis - Runners — máquinas virtuais onde os jobs rodam
Workflow de CI Completo
# .github/workflows/ci.yml
name: CI — Testes e Qualidade
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch: # permite execução manual
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # cancela runs anteriores do mesmo PR
env:
PYTHON_VERSION: "3.12"
POETRY_VERSION: "1.8.0"
jobs:
# ── JOB 1: Qualidade de Código ────────────────────────
qualidade:
name: Qualidade de Código
runs-on: ubuntu-latest
steps:
- name: Checkout do código
uses: actions/checkout@v4
- name: Configurar Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: pip
- name: Instalar dependências de qualidade
run: pip install ruff black mypy bandit
- name: Ruff — linting
run: ruff check src/ tests/
- name: Black — formatação
run: black --check src/ tests/
- name: mypy — type checking
run: mypy src/ --ignore-missing-imports
- name: Bandit — segurança
run: bandit -r src/ -ll # só erros médios e altos
# ── JOB 2: Testes ─────────────────────────────────────
testes:
name: Testes Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
needs: qualidade # só roda se qualidade passar
strategy:
fail-fast: false # não cancela outras versões se uma falhar
matrix:
python-version: ["3.11", "3.12"]
os: [ubuntu-latest, windows-latest, macos-latest]
exclude:
# Evita combinações desnecessárias
- os: windows-latest
python-version: "3.11"
- os: macos-latest
python-version: "3.11"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configurar Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Instalar dependências
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Executar testes com cobertura
run: |
pytest tests/ \
--cov=src \
--cov-report=xml \
--cov-report=term-missing \
--cov-fail-under=85 \
-v \
--tb=short
- name: Enviar cobertura ao Codecov
uses: codecov/codecov-action@v4
if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
fail_ci_if_error: false
# ── JOB 3: Testes de Integração ───────────────────────
integracao:
name: Testes de Integração
runs-on: ubuntu-latest
needs: testes
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
REDIS_URL: redis://localhost:6379/0
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: pip
- run: pip install -r requirements.txt -r requirements-dev.txt
- name: Executar migrações
run: alembic upgrade head
- name: Testes de integração
run: pytest tests/integration/ -v --tb=short -m integration
# ── JOB 4: Build da Imagem Docker ─────────────────────
docker:
name: Build Docker
runs-on: ubuntu-latest
needs: [testes, integracao]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Configurar Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login no GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extrair metadados
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=sha,prefix=sha-
type=semver,pattern={{version}}
- name: Build e Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Workflow de CD: Deploy Automático
# .github/workflows/cd.yml
name: CD — Deploy
on:
push:
branches: [main]
tags:
- "v*.*.*"
workflow_run:
workflows: ["CI — Testes e Qualidade"]
types: [completed]
branches: [main]
jobs:
deploy-staging:
name: Deploy para Staging
runs-on: ubuntu-latest
if: >
github.event_name == 'push' ||
github.event.workflow_run.conclusion == 'success'
environment:
name: staging
url: https://staging.escola.com
steps:
- uses: actions/checkout@v4
- name: Configurar SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_STAGING_KEY }}
- name: Deploy para Staging
run: |
ssh ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} << 'EOF'
cd /app/escola
git pull origin main
docker compose pull
docker compose up -d --no-deps --build api
docker compose exec api alembic upgrade head
echo "Deploy staging concluído!"
EOF
- name: Smoke Tests no Staging
run: |
sleep 10 # aguarda o serviço iniciar
curl -f https://staging.escola.com/health || exit 1
curl -f https://staging.escola.com/alunos || exit 1
- name: Notificar Slack
if: always()
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "Deploy Staging: ${{ job.status }} — ${{ github.sha }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
deploy-producao:
name: Deploy para Produção
runs-on: ubuntu-latest
needs: deploy-staging
if: startsWith(github.ref, 'refs/tags/v')
environment:
name: production
url: https://escola.com
steps:
- uses: actions/checkout@v4
- name: Aprovação manual
uses: trstringer/manual-approval@v1
with:
secret: ${{ secrets.GITHUB_TOKEN }}
approvers: prof-ricardo-matos,tech-lead
minimum-approvals: 1
issue-title: "Deploy v${{ github.ref_name }} para produção"
- name: Deploy para Produção
run: |
echo "Deployando ${{ github.ref_name }} para produção..."
- name: Criar Release no GitHub
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
token: ${{ secrets.GITHUB_TOKEN }}
Secrets e Variáveis de Ambiente
# Usando secrets no workflow
steps:
- name: Configurar credenciais
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
API_KEY: ${{ secrets.EXTERNAL_API_KEY }}
run: echo "Credenciais configuradas"
# Secrets do ambiente específico (staging/producao)
- name: Deploy com credenciais do ambiente
environment: staging
env:
HOST: ${{ secrets.STAGING_HOST }}
run: ./scripts/deploy.sh
# Configurando secrets via GitHub CLI
gh secret set DATABASE_URL --body "postgresql://..."
gh secret set SECRET_KEY --body "minha-chave-secreta"
# Listando secrets
gh secret list
# Secrets por ambiente
gh secret set DATABASE_URL \
--env staging \
--body "postgresql://staging-host/db"
Cache de Dependências
# Cache inteligente — só reinstala se requirements mudar
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
# Cache para Poetry
- name: Cache Poetry
uses: actions/cache@v4
with:
path: |
~/.cache/pypoetry
.venv
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
# Cache para Docker layers
- name: Cache Docker
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
Workflow de Release Automatizado
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # histórico completo para changelog
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Instalar dependências
run: pip install build twine
- name: Build do pacote
run: python -m build
- name: Verificar distribuição
run: twine check dist/*
- name: Publicar no PyPI
if: startsWith(github.ref, 'refs/tags/')
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: twine upload dist/*
- name: Gerar Changelog
id: changelog
uses: mikepenz/release-changelog-builder-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Criar GitHub Release
uses: softprops/action-gh-release@v2
with:
body: ${{ steps.changelog.outputs.changelog }}
files: dist/*
token: ${{ secrets.GITHUB_TOKEN }}
Workflow Agendado: Tarefas Periódicas
# .github/workflows/schedule.yml
name: Tarefas Agendadas
on:
schedule:
- cron: "0 6 * * 1-5" # 6h, seg a sex
- cron: "0 2 * * 0" # 2h todo domingo
jobs:
backup:
name: Backup Semanal
runs-on: ubuntu-latest
if: github.event.schedule == '0 2 * * 0'
steps:
- uses: actions/checkout@v4
- run: python scripts/backup.py
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
relatorio-diario:
name: Relatório Diário
runs-on: ubuntu-latest
if: github.event.schedule == '0 6 * * 1-5'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: |
pip install -r requirements.txt
python scripts/gerar_relatorio.py
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
seguranca:
name: Auditoria de Dependências
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: pip-audit
run: |
pip install pip-audit
pip-audit -r requirements.txt --format=json \
-o relatorio_seguranca.json || true
- uses: actions/upload-artifact@v4
with:
name: relatorio-seguranca
path: relatorio_seguranca.json
Reutilizando Workflows
# .github/workflows/_setup-python.yml — workflow reutilizável
name: Setup Python (reutilizável)
on:
workflow_call:
inputs:
python-version:
required: false
type: string
default: "3.12"
secrets:
CODECOV_TOKEN:
required: false
jobs:
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: pip
- run: pip install -r requirements.txt -r requirements-dev.txt
# Usando o workflow reutilizável
jobs:
testes:
uses: ./.github/workflows/_setup-python.yml
with:
python-version: "3.12"
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
Exemplo Completo: Pipeline de Produção
# .github/workflows/pipeline-completo.yml
name: Pipeline Completo
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
packages: write
pull-requests: write
jobs:
# ── Análise de Código ──────────────────────────────
analise:
name: Análise Estática
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install ruff black mypy bandit
- run: ruff check src/ tests/ --output-format=github
- run: black --check src/ tests/
- run: mypy src/ --ignore-missing-imports --no-error-summary
- run: bandit -r src/ -ll -q
# ── Testes ────────────────────────────────────────
testes:
name: Testes e Cobertura
runs-on: ubuntu-latest
needs: analise
outputs:
coverage: ${{ steps.cov.outputs.coverage }}
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: Executar testes
id: cov
run: |
pytest tests/unit/ \
--cov=src \
--cov-report=xml \
--cov-report=json \
--cov-fail-under=85 \
-q
COBERTURA=$(python -c "import json; d=json.load(open('coverage.json')); print(f\"{d['totals']['percent_covered']:.1f}\")")
echo "coverage=$COBERTURA" >> $GITHUB_OUTPUT
- name: Comentar cobertura no PR
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
message: |
## 📊 Cobertura de Testes
**${{ steps.cov.outputs.coverage }}%** de cobertura
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov/
# ── Build Docker ──────────────────────────────────
build:
name: Build e Push Docker
runs-on: ubuntu-latest
needs: testes
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build e Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ── Deploy Staging ────────────────────────────────
staging:
name: Deploy Staging
runs-on: ubuntu-latest
needs: build
environment:
name: staging
url: https://staging.escola.com
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.SSH_STAGING_KEY }}
script: |
cd /app/escola
docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
docker compose up -d --no-deps \
-e IMAGE_TAG=${{ github.sha }} api
sleep 5
curl -f http://localhost:8000/health
- name: Notificar sucesso
if: success()
run: |
echo "✅ Deploy staging concluído: ${{ github.sha }}"
# ── Sumário ───────────────────────────────────────
sumario:
name: Sumário do Pipeline
runs-on: ubuntu-latest
needs: [analise, testes, build, staging]
if: always()
steps:
- name: Gerar sumário
run: |
echo "## 📋 Resultado do Pipeline" >> $GITHUB_STEP_SUMMARY
echo "| Etapa | Status |" >> $GITHUB_STEP_SUMMARY
echo "|--------------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Análise | ${{ needs.analise.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Testes | ${{ needs.testes.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build Docker | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Staging | ${{ needs.staging.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Cobertura | ${{ needs.testes.outputs.coverage }}% |" >> $GITHUB_STEP_SUMMARY
Boas Práticas de CI/CD
# 1. Sempre use versões fixas das actions
uses: actions/checkout@v4 # ✓
uses: actions/checkout@main # ✗ — pode mudar
# 2. Minimize permissões
permissions:
contents: read # só leitura por padrão
packages: write # só onde necessário
# 3. Use concurrency para evitar deploys simultâneos
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false # não cancela deploys em andamento
# 4. Artefatos para debug
- uses: actions/upload-artifact@v4
if: failure() # só sobe se falhar
with:
name: logs-falha
path: |
logs/
*.log
retention-days: 7
# 5. Timeouts para evitar jobs travados
jobs:
testes:
timeout-minutes: 30
# 6. Condicionais claras
- name: Deploy produção
if: >
github.ref == 'refs/heads/main' &&
github.event_name == 'push' &&
needs.testes.result == 'success'
Resumo
- GitHub Actions usa arquivos YAML em
.github/workflows/— cada arquivo é um workflow - Triggers definem quando o workflow executa — push, PR, tag, schedule ou manual
- Jobs rodam em paralelo por padrão;
needscria dependências e sequência - Matrix strategy permite testar em múltiplas versões de Python e sistemas operacionais
- Services sobem containers de banco de dados e redis para testes de integração
- Secrets armazenam credenciais — nunca exponha valores sensíveis no YAML
- Cache de dependências reduz o tempo de execução em 60-80% nas execuções subsequentes
- Environments com aprovação manual protegem o deploy em produção contra acidentais
Referências e Leituras Complementares
- GitHub Actions — documentação oficial — https://docs.github.com/en/actions
- Marketplace de Actions — https://github.com/marketplace?type=actions
- actions/setup-python — https://github.com/actions/setup-python
- docker/build-push-action — https://github.com/docker/build-push-action
- Codecov — cobertura de testes — https://about.codecov.io/
- pip-audit — auditoria de dependências — https://github.com/pypa/pip-audit
- HUMBLE, Jez; FARLEY, David. Continuous Delivery. Addison-Wesley, 2010. — a referência original sobre entrega contínua de software.
- KIM, Gene et al. The DevOps Handbook. 2. ed. IT Revolution, 2021. — fundamentos e práticas de DevOps aplicadas a organizações reais.
Prof. Ricardo Matos — Dominando o Python em 1 Ano · Artigo 45 de 52 Próximo: Artigo 46 — Logging, Monitoramento e Observabilidade
you asked
Sim