Pular para o conteúdo
devops

CI/CD com GitHub Actions: Pipeline de Deploy do Zero à Produção

Crie um pipeline CI/CD completo com GitHub Actions: testes automatizados, build Docker, deploy em Kubernetes ou VPS, rollback e notificações.

Douglas M. Pereira4 min de leitura
ci/cdgithub actionsdevopsdeploydockerkubernetesautomação

Por que CI/CD é inegociável em 2025

Deployar manualmente via FTP, SSH ou painel de hospedagem em 2025 é um pesadelo operacional. Cada deploy é um risco de erro humano, cada hotfix é uma corrida contra o relógio manual. CI/CD automatiza tudo: testes rodam em cada pull request, builds são imutáveis, deploys são repetiveis e o rollback é um comando.

GitHub Actions é a ferramenta padrão para a maioria dos projetos — integrado ao GitHub, gratuito para projetos open source, tem mercado massivo de actions prontas.

Anatomia de um pipeline completo

Developer push → PR aberto
       ↓
   [CI Pipeline]
   ├── Lint (ESLint/Biome)
   ├── Type check (tsc)
   ├── Testes unitários
   ├── Testes de integração
   └── Build (verificação)
       ↓ (PR aprovado + merge na main)
   [CD Pipeline]        
   ├── Build imagem Docker
   ├── Push para registry
   ├── Deploy em staging
   ├── Smoke tests
   └── Deploy em produção (com aprovação manual)

Pipeline básico: CI para Node.js

# .github/workflows/ci.yml
name: CI

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

jobs:
  test:
    name: Test & Lint
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      
      redis:
        image: redis:7
        ports: ["6379:6379"]
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"
      
      - name: Install dependencies
        run: npm ci
      
      - name: Type check
        run: npm run type-check
      
      - name: Lint
        run: npm run lint
      
      - name: Tests
        run: npm test -- --coverage --reporter=verbose
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379
          JWT_SECRET: test-secret-key-for-ci
      
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Pipeline CD: Build Docker e deploy

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]
    tags: ["v*"]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    name: Build & Push Docker Image
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=sha,prefix={{branch}}-
            type=semver,pattern={{version}}
      
      - name: Build and 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
  
  deploy-staging:
    name: Deploy to Staging
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: staging
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to Kubernetes
        uses: azure/k8s-deploy@v5
        with:
          manifests: k8s/staging/
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image-tag }}
          kubeconfig: ${{ secrets.KUBE_CONFIG_STAGING }}
      
      - name: Run smoke tests
        run: |
          sleep 30  # aguardar pods subirem
          curl -f https://staging.minha-empresa.com/health || exit 1
  
  deploy-production:
    name: Deploy to Production
    needs: [build-and-push, deploy-staging]
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.minha-empresa.com
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to production
        uses: azure/k8s-deploy@v5
        with:
          manifests: k8s/production/
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image-tag }}
          kubeconfig: ${{ secrets.KUBE_CONFIG_PRODUCTION }}
          strategy: rolling

Protegendo Secrets

# Nunca commite secrets — sempre use GitHub Secrets
# gh CLI para adicionar secrets:
gh secret set DATABASE_URL --body "postgresql://..."
gh secret set JWT_SECRET --body "super-secret"

# Environments para isolar por ambiente
# Settings > Environments > production
# Configure: required reviewers antes de deploy em produção

Deploy em VPS (alternativa ao K8s)

# .github/workflows/deploy-vps.yml (para VPS com Docker Compose)
deploy-vps:
  runs-on: ubuntu-latest
  needs: build-and-push
  
  steps:
    - name: Deploy via SSH
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.VPS_HOST }}
        username: deploy
        key: ${{ secrets.VPS_SSH_KEY }}
        script: |
          cd /opt/app
          
          # Atualizar imagem
          export IMAGE_TAG=${{ needs.build-and-push.outputs.image-tag }}
          docker pull ghcr.io/empresa/api:$IMAGE_TAG
          
          # Rolling update zero-downtime
          docker compose up -d --no-deps api
          
          # Health check
          sleep 10
          docker compose ps api | grep -q "Up" || (docker compose logs api && exit 1)
          
          echo "Deploy concluído: $IMAGE_TAG"

Notificações de deploy

- name: Notify Slack on success
  if: success()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "✅ Deploy em produção: ${{ github.event.head_commit.message }} por ${{ github.actor }}"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

- name: Notify Slack on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "🚨 FALHA no deploy: ${{ github.run_url }}"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}