Pular para o conteúdo
iauaiiauai — portal de tecnologia, IA e Cloud
EngenhariaIniciante

CI/CD com GitHub Actions do zero

Workflow GitHub Actions: lint, testes, build, deploy. Cache de dependências, matriz de versões, secrets seguros, aprovação manual, armadilhas de custo.

Por Equipe iauai · 7 de agosto de 2026 · 12 min de leitura

Nesta página

O problema real

Você tem um repositório GitHub, faz commit, e dois desenvolvedores testam localmente. Um passa, outro falha — "funciona no meu laptop". Código vai pra produção, quebra. Ou você roda testes manualmente antes de fazer deploy, e tá aí 30 minutos esperando para liberar uma mudança simples de uma linha.

A realidade: sem CI/CD, cada desenvolvedor testa de forma diferente. Sem CD (continuous deployment), ninguém sabe quando tá seguro deploy. GitHub Actions resolve: toda mudança que entra é testada automaticamente, e se passar, pode ir direto pra produção sem click manual.

Anatomia de um workflow: on, jobs, steps, runs-on

Um workflow é arquivo YAML em .github/workflows/. Estrutura básica:

name: CI/CD Pipeline                    # nome visual
on:                                      # quando disparar?
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:                                    # conjunto de tarefas
  test:                                  # primeira tarefa
    runs-on: ubuntu-latest               # qual máquina?
    steps:
      - name: Checkout code              # descarga repo
        uses: actions/checkout@v4

      - name: Setup Node                 # instala Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Run tests                  # executa testes
        run: npm test

Registrador: GitHub mostra cada step, com tempo e output. Se um falha, marca tudo em vermelho e avisa via email/Slack.

Pipeline completo: lint → testes → build → deploy

name: Full CI/CD

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

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

jobs:
  lint:
    name: Lint & Format Check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

      - name: Check formatting with Prettier
        run: npm run format:check

  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint                          # roda SÓ se lint passar
    strategy:
      matrix:
        node-version: [16, 18, 20]       # testa 3 versões do Node
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm test -- --coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

  build:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: test                          # só se testes passarem
    if: github.event_name == 'push'     # só em push, não em PR
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

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

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        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:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'   # só branch develop
    steps:
      - name: Deploy to Staging
        run: |
          curl -X POST ${{ secrets.DEPLOY_WEBHOOK }} \
            -H "Content-Type: application/json" \
            -d '{"image":"${{ needs.build.outputs.image }}"}'

  deploy-prod:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'      # só branch main
    environment:
      name: production
      url: https://app.example.com
    steps:
      - name: Manual approval required
        run: echo "Waiting for approval..."

      - name: Deploy to Production
        run: |
          curl -X POST ${{ secrets.PROD_DEPLOY_WEBHOOK }} \
            -H "Content-Type: application/json" \
            -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
            -d '{"environment":"production"}'

      - name: Notify deployment
        if: success()
        run: |
          echo "✓ Deployed to production"

O que acontece:

  1. on push dispara todo o pipeline
  2. lint roda em paralelo (runs-on: ubuntu-latest = máquina Ubuntu)
  3. test roda SÓ se lint passou (needs: lint)
  4. test roda em 3 versões de Node simultaneamente (matrix.node-version)
  5. build roda SÓ se testes passaram
  6. deploy e deploy-prod são condicionais por branch
  7. Cache economiza minutos (veja seção abaixo)

Cache de dependências e quanto economiza

Cache reutiliza node_modules entre rodadas:

- name: Cache dependencies
  uses: actions/cache@v3
  with:
    path: ~/.npm                              # cache local do npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

Teste real:

  • Sem cache: npm ci = 45 segundos
  • Com cache, hit: npm ci = 3 segundos (lê cache)
  • Com cache, miss: npm ci = 45 segundos (regenera cache)

Por 10 runs/dia: (45-3) * 10 = 420 segundos = 7 minutos economizados/dia. Em um mês: 3,5 horas.

Matriz de versões

Testar em múltiplas versões de linguagem com strategy.matrix:

strategy:
  matrix:
    node-version: [16, 18, 20]
    os: [ubuntu-latest, macos-latest, windows-latest]

Isso cria 9 combinações de jobs. Cada um roda isolado. Seu código roda em Node 16, 18 e 20 em Linux, Mac e Windows. Número real: encontra incompatibilidades antes do usuário.

Secrets e por que nunca ir para o log

❌ NUNCA hardcode secrets:

# ERRADO: mesmo que você apague depois, GitHub guarda história
- name: Deploy
  run: curl https://api.com -H "Authorization: Bearer abc123def456"

GitHub procura por padrões de secrets no commit e avisa. Mas já vaza pra log publicamente.

✓ USE secrets:

- name: Deploy
  run: |
    curl https://api.com \
      -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}"

Secrets são:

  1. Definidos em Settings > Secrets and variables > Actions
  2. Não aparecem em logs (GitHub substitui por ***)
  3. Somente acessíveis em workflows
  4. Criptografados em repouso

Real:

# Log publicamente visível
Run: curl -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}"

# Resultado: Authorization: Bearer ***

Ambientes e aprovação manual

Para produção, aprove manualmente antes de deploy:

deploy-prod:
  name: Deploy to Production
  runs-on: ubuntu-latest
  environment:
    name: production
    url: https://app.example.com
  steps:
    - name: Deploy
      run: ./deploy.sh

GitHub aguarda aprovação. Clica na aba "Deployments", vê a request, aprova ou rejeita. Até 30 dias de espera.

Jobs condicionais por branch

Roda apenas em branches específicas:

deploy:
  if: github.ref == 'refs/heads/develop'  # só develop
  runs-on: ubuntu-latest
  steps:
    - name: Deploy to staging
      run: ./deploy-staging.sh

deploy-prod:
  if: github.ref == 'refs/heads/main'     # só main
  runs-on: ubuntu-latest
  steps:
    - name: Deploy to production
      run: ./deploy-prod.sh

Ou por evento (push vs PR):

if: github.event_name == 'push'           # só em push
if: github.event_name == 'pull_request'   # só em PR

Armadilhas comuns

1. Workflow que roda em todo push e estoura minuto mensal gratuito:

GitHub Actions: 2.000 minutos/mês grátis (privado). Um workflow de 45 minutos por push em 50 pushes/dia = 2.250 minutos. Paga.

Solução:

on:
  push:
    branches: [main, develop]  # só branches importantes
    paths-ignore:
      - 'docs/**'              # não rode se muda só docs
      - '*.md'

2. Segredo exposto em PR de fork:

Fork externo faz PR, workflow roda, e não tem acesso a secrets da repo principal (por padrão, segurança). Mas push da main tem. Risco: alguém injeta step que faz log de secret.

Proteção:

permissions:
  contents: read              # read-only
  pull-requests: read

3. Deploy sem rollback:

Se deploy quebra produção, ninguém volta. Sempre tenha reversão automática.

- name: Deploy
  run: ./deploy.sh

- name: Health check
  run: curl -f https://app.example.com/health || ./rollback.sh

Quando NÃO usar GitHub Actions

Projeto com 50+ deploy/dia: Actions fica caro (paga minuto adicional). Use Jenkins ou GitLab CI (mais barato em scale).

Build que demora 2 horas: máquinas Actions são ubuntu-latest com 4 CPUs. Compile localmente, cacheia, ou use runner dedicado (pago).

Segredos sensíveis demais: se precisa de vaults externos (HashiCorp Vault, AWS Secrets Manager), integre, mas complexidade cresce. Considere sistema dedicado.

Próximos passos

Empacotar imagens Docker durante CI/CD com Docker: imagens menores com multi-stage — otimize build time.

Escalável e com infra em código: Terraform na prática: sua primeira infra versionada.

Crie um repositório novo no GitHub, coloque o workflow acima, faça commit, e vê rodar em tempo real na aba Actions. Clique em cada step e veja logs. Quebrar uma coisa propositalmente (remova um import do código, vê test falhar). Esse feedback visual é ouro para aprender.

Continue lendo