Python

Artigo 45 — CI/CD com GitHub Actions Já leu

12 min de leitura

Artigo 45 — CI/CD com GitHub Actions
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,

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; needs cria 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


claude response

Dominando o Python em 1 Ano

Comentários

Mais em Python

Celery: Filas de Tarefas e Processamento Assíncrono
Celery: Filas de Tarefas e Processamento Assíncrono

Algumas operações são pesadas demais para acontecer durante uma requisição HT...

Listas: criação, manipulação e métodos
Listas: criação, manipulação e métodos

Listas s&atilde;o a estrutura de dados mais utilizada em Python. Elas permite...

Operadores e Expressões
Operadores e Expressões

Se vari&aacute;veis s&atilde;o os recipientes que guardam dados, os operadore...