DevOps

Pipeline com GitHub Actions: Build, Test e Deploy Automático Já leu

9 min de leitura

Pipeline com GitHub Actions: Build, Test e Deploy Automático
O artigo anterior estabeleceu os fundamentos conceituais do CI/CD. Este artigo é inteiramente prático: será construído um pipeline completo e funcional usando GitHub Actions, cobrindo todas as etapas de um fluxo de entre

O artigo anterior estabeleceu os fundamentos conceituais do CI/CD. Este artigo é inteiramente prático: será construído um pipeline completo e funcional usando GitHub Actions, cobrindo todas as etapas de um fluxo de entrega real — desde a execução de testes até o deploy automático em um servidor.

O projeto de exemplo será uma API Node.js com Express, PostgreSQL e testes automatizados. A estrutura é simples o suficiente para ser compreendida completamente, mas realista o suficiente para representar o que se encontra em projetos de produção.


Estrutura do Projeto de Exemplo

minha-api/
├── src/
│   ├── index.js
│   ├── routes/
│   │   └── users.js
│   └── db/
│       └── connection.js
├── tests/
│   ├── unit/
│   │   └── users.test.js
│   └── integration/
│       └── users.api.test.js
├── Dockerfile
├── docker-compose.yml
├── docker-compose.test.yml
├── package.json
└── .github/
    └── workflows/
        ├── ci.yml
        └── deploy.yml

O Arquivo de CI: Qualidade em Cada Push

O primeiro workflow garante qualidade do código a cada push ou Pull Request:

# .github/workflows/ci.yml
name: CI — Integração Contínua

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

env:
  NODE_VERSION: '20.x'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # ── Job 1: Qualidade de Código ───────────────────
  lint-e-analise:
    name: Lint e Análise Estática
    runs-on: ubuntu-latest

    steps:
      - name: Clona o repositório
        uses: actions/checkout@v4

      - name: Configura Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Instala dependências
        run: npm ci

      - name: Executa o linter
        run: npm run lint

      - name: Verifica formatação com Prettier
        run: npm run format:check

  # ── Job 2: Testes Unitários ──────────────────────
  testes-unitarios:
    name: Testes Unitários
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci

      - name: Executa testes unitários com cobertura
        run: npm run test:unit -- --coverage

      - name: Publica relatório de cobertura
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7

      - name: Verifica cobertura mínima
        run: |
          COVERAGE=$(cat coverage/coverage-summary.json | \
            node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); \
            console.log(JSON.parse(d).total.lines.pct)")
          echo "Cobertura de linhas: $COVERAGE%"
          node -e "if ($COVERAGE < 80) { \
            console.error('Cobertura abaixo de 80%'); process.exit(1); }"

  # ── Job 3: Testes de Integração ──────────────────
  testes-integracao:
    name: Testes de Integração
    runs-on: ubuntu-latest
    needs: testes-unitarios

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test123
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci

      - name: Executa migrações de teste
        env:
          DATABASE_URL: postgresql://test:test123@localhost:5432/testdb
        run: npm run db:migrate

      - name: Executa testes de integração
        env:
          DATABASE_URL: postgresql://test:test123@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379
          NODE_ENV: test
        run: npm run test:integration

  # ── Job 4: Build da Imagem Docker ────────────────
  build-docker:
    name: Build da Imagem Docker
    runs-on: ubuntu-latest
    needs: [ lint-e-analise, testes-integracao ]
    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
      - uses: actions/checkout@v4

      - name: Configura Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login no GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extrai metadados
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=sha,prefix=sha-
            type=semver,pattern={{version}}

      - name: Escaneia vulnerabilidades antes do build
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          severity: CRITICAL
          exit-code: 1

      - name: Constrói e publica imagem
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

O Arquivo de Deploy: Entregando em Produção

O segundo workflow cuida do deploy após o merge na main:

# .github/workflows/deploy.yml
name: Deploy — Entrega Contínua

on:
  workflow_run:
    workflows: ["CI — Integração Contínua"]
    types: [completed]
    branches: [main]

jobs:
  deploy-staging:
    name: Deploy em Staging
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    environment:
      name: staging
      url: https://staging.minha-api.com

    steps:
      - uses: actions/checkout@v4

      - name: Configura acesso SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.STAGING_SSH_KEY }}

      - name: Adiciona host ao known_hosts
        run: |
          ssh-keyscan -H ${{ secrets.STAGING_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy via SSH
        env:
          IMAGE: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
          HOST: ${{ secrets.STAGING_HOST }}
          USER: ${{ secrets.STAGING_USER }}
        run: |
          ssh $USER@$HOST << EOF
            # Atualiza a imagem
            docker pull $IMAGE

            # Para o container atual
            docker stop api-staging || true
            docker rm api-staging || true

            # Sobe o novo container
            docker run -d \
              --name api-staging \
              --network rede-producao \
              -e NODE_ENV=staging \
              -e DATABASE_URL=${{ secrets.STAGING_DATABASE_URL }} \
              -e REDIS_URL=${{ secrets.STAGING_REDIS_URL }} \
              --restart unless-stopped \
              -p 3000:3000 \
              $IMAGE

            # Aguarda o healthcheck
            sleep 10
            docker inspect --format='{{.State.Health.Status}}' api-staging
          EOF

      - name: Verifica deploy
        run: |
          sleep 15
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
            https://staging.minha-api.com/health)
          if [ "$STATUS" != "200" ]; then
            echo "Deploy falhou — status HTTP: $STATUS"
            exit 1
          fi
          echo "Deploy bem-sucedido — status HTTP: $STATUS"

  deploy-producao:
    name: Deploy em Produção
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://api.minha-api.com

    steps:
      - uses: actions/checkout@v4

      - name: Configura acesso SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.PROD_SSH_KEY }}

      - name: Adiciona host ao known_hosts
        run: |
          ssh-keyscan -H ${{ secrets.PROD_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy com zero downtime
        env:
          IMAGE: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
          HOST: ${{ secrets.PROD_HOST }}
          USER: ${{ secrets.PROD_USER }}
        run: |
          ssh $USER@$HOST << 'EOF'
            set -e

            IMAGE="${{ env.IMAGE }}"

            # Baixa a nova imagem antes de parar a atual
            docker pull $IMAGE

            # Sobe o novo container na porta alternativa
            docker run -d \
              --name api-prod-new \
              --network rede-producao \
              -e NODE_ENV=production \
              -e DATABASE_URL=$PROD_DATABASE_URL \
              -e REDIS_URL=$PROD_REDIS_URL \
              --restart unless-stopped \
              -p 3001:3000 \
              $IMAGE

            # Aguarda o novo container ficar saudável
            for i in $(seq 1 12); do
              HEALTH=$(docker inspect --format='{{.State.Health.Status}}' api-prod-new 2>/dev/null)
              if [ "$HEALTH" = "healthy" ]; then
                echo "Novo container saudável após ${i}0 segundos"
                break
              fi
              echo "Aguardando... ($i/12)"
              sleep 10
            done

            if [ "$HEALTH" != "healthy" ]; then
              echo "Novo container não ficou saudável — abortando deploy"
              docker rm -f api-prod-new
              exit 1
            fi

            # Redireciona tráfego para o novo container (via nginx reload)
            sudo nginx -s reload

            # Para e remove o container antigo
            docker stop api-prod || true
            docker rm api-prod || true

            # Renomeia o novo para o nome padrão
            docker rename api-prod-new api-prod

            echo "Deploy concluído com sucesso"
          EOF

      - name: Notifica o time no Slack
        if: always()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {
              "text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy em produção: ${{ job.status }}\nCommit: ${{ github.sha }}\nAutor: ${{ github.actor }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Environments: Ambientes com Aprovação Manual

O GitHub Actions oferece o conceito de environments — ambientes configuráveis com regras de proteção, secrets específicos e URL de destino. Para o ambiente de produção, é possível exigir aprovação manual antes do deploy:

No repositório, navega-se até Settings → Environments → New environment. Cria-se o ambiente production e ativa-se a opção Required reviewers, adicionando os membros da equipe que podem aprovar deploys.

Com isso configurado, o pipeline pausa antes do job de deploy em produção e envia uma notificação para os revisores aprovarem. O deploy só prossegue após a aprovação explícita.


Reutilizando Workflows com Reusable Workflows

Em projetos com múltiplos repositórios, duplicar o mesmo workflow de CI em cada um é impraticável. O GitHub Actions suporta reusable workflows — workflows que podem ser chamados por outros workflows:

# .github/workflows/ci-reusavel.yml — no repositório central
name: CI Reutilizável

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20.x'
    secrets:
      REGISTRY_TOKEN:
        required: true

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test
# Em qualquer outro repositório
name: CI

on: [push]

jobs:
  ci:
    uses: minha-org/workflows/.github/workflows/ci-reusavel.yml@main
    with:
      node-version: '20.x'
    secrets:
      REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Depurando Pipelines que Falham

Quando um pipeline falha e a saída dos logs não é suficiente para diagnosticar o problema, o GitHub Actions oferece a possibilidade de abrir uma sessão SSH interativa dentro do runner usando a action tmate:

- name: Abre sessão de debug SSH
  uses: mxschmitt/action-tmate@v3
  if: ${{ failure() }}
  with:
    limit-access-to-actor: true

Quando o step anterior falhar, esse step abre uma sessão SSH temporária e imprime o endereço de conexão nos logs. O desenvolvedor pode então conectar ao runner e inspecionar o ambiente como se fosse uma máquina qualquer — verificar arquivos, rodar comandos, investigar variáveis de ambiente.


O Que Vem a Seguir

O próximo artigo aprofunda um tema central do pipeline construído aqui: testes automatizados. Serão abordadas as diferentes categorias de testes, como organizá-los para máxima eficiência no pipeline, como lidar com testes flaky e como escrever testes que realmente dão confiança para fazer deploy.


Referências para Aprofundamento

Documentação oficial - GitHub Actions — Workflow Syntax — Referência completa de toda a sintaxe disponível em arquivos de workflow. - GitHub Actions — Reusable Workflows — Documentação completa sobre como criar e consumir workflows reutilizáveis. - GitHub Actions — Environments — Guia completo sobre configuração de ambientes com proteções e aprovações manuais.

Ferramentas usadas no artigo - webfactory/ssh-agent — GitHub — Action para configurar o agente SSH no runner com suporte a múltiplas chaves. - mxschmitt/action-tmate — GitHub — Action para abrir sessões de debug SSH interativas dentro de runners do GitHub Actions. - slackapi/slack-github-action — GitHub — Action oficial do Slack para enviar notificações de workflows.

Leitura complementar - Act — Rodando GitHub Actions Localmente — Ferramenta para executar workflows do GitHub Actions localmente antes de fazer push, acelerando o ciclo de desenvolvimento do pipeline.

Comentários

Mais em DevOps

Git na Prática: Commits, Branches e Merges sem Medo
Git na Prática: Commits, Branches e Merges sem Medo

Antes do Git, equipes de desenvolvimento compartilhavam c&oacute;digo por e-m...

Compliance e Auditoria: LGPD, SOC2 e Evidências Automatizadas
Compliance e Auditoria: LGPD, SOC2 e Evidências Automatizadas

Existe uma diferença fundamental entre uma organização que é segura e uma org...

GitLab CI/CD: A Alternativa Enterprise ao GitHub Actions
GitLab CI/CD: A Alternativa Enterprise ao GitHub Actions

O GitHub é a plataforma padrão para projetos open-source e para a maioria dos...