Toda vez que faço push, meu código é testado, buildado e deployado automaticamente. Zero intervenção manual. Se você ainda faz deploy manualmente, este tutorial é para você.

Neste artigo, vou mostrar como montar um pipeline CI/CD completo usando GitHub Actions — desde o workflow YAML mais básico até um deploy automatizado em produção. Vamos usar um projeto Node.js como exemplo, mas os conceitos se aplicam a qualquer stack.

O que é CI/CD — explicação simples

CI (Continuous Integration) significa que toda vez que alguém faz push ou abre um pull request, o código é automaticamente testado. Se os testes passam, o código está íntegro. Se falham, você descobre imediatamente — não três semanas depois quando alguém faz merge de um branch quebrado.

CD (Continuous Deployment) vai além: depois que os testes passam, o código é automaticamente deployado em produção. Sem precisar de alguém logando no servidor, rodando scripts manuais ou rezando para nada quebrar.

A combinação dos dois cria um ciclo onde cada commit validado chega ao usuário final em minutos, não dias.

Por que GitHub Actions

Existem dezenas de ferramentas de CI/CD (Jenkins, GitLab CI, CircleCI, Travis CI…), mas GitHub Actions tem vantagens claras:

  • Integrado ao GitHub: não precisa configurar serviço externo, webhooks ou integrações. Está tudo no mesmo lugar onde seu código vive.
  • Free tier generoso: 2.000 minutos/mês para repositórios públicos e 500 minutos/mês para privados. Para a maioria dos projetos, você nunca paga nada.
  • Marketplace com milhares de actions reutilizáveis: precisa fazer deploy na AWS? Há uma action. Precisa enviar notificação no Slack? Há uma action. A comunidade já resolveu a maioria dos problemas comuns.
  • YAML versionado com seu código: o pipeline é um arquivo no repositório. Muda junto com o código, passa por code review e tem histórico no Git.

Anatomia de um workflow

Todo workflow fica em .github/workflows/ na raiz do repositório. É um arquivo YAML com esta estrutura:

name: Nome do Workflow        # Nome exibido na aba Actions
on: [push]                    # Quando executar (trigger)
jobs:                         # Lista de jobs
  meu-job:                    # Identificador do job
    runs-on: ubuntu-latest    # Máquina virtual onde roda
    steps:                    # Passos sequenciais
      - uses: actions/checkout@v4   # Action reutilizável
      - run: echo "Hello World"     # Comando shell

Triggers — quando o workflow executa

O campo on define quando o pipeline roda:

  • push — a cada push em branches específicos
  • pull_request — quando um PR é aberto ou atualizado
  • schedule — cron job (ex: rodar todo dia às 7h)
  • workflow_dispatch — botão manual na interface do GitHub

Jobs e Steps

Um workflow pode ter múltiplos jobs que rodam em paralelo (por padrão). Cada job contém steps que rodam sequencialmente. Se um step falha, os seguintes não executam.

Runners

O runs-on define a máquina virtual:

  • ubuntu-latest — Linux (mais comum, mais rápido)
  • windows-latest — Windows Server
  • macos-latest — macOS (útil para apps iOS/macOS)

Tutorial: Pipeline completo para um app Node.js

Vamos construir um pipeline incrementalmente — do teste básico ao deploy em produção.

Step 1: Workflow de testes

Crie o arquivo .github/workflows/ci.yml:

name: CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm test

O que cada step faz:

  1. checkout — clona o repositório na VM
  2. setup-node — instala Node.js 22 e configura cache do npm
  3. npm ci — instala dependências (mais rápido e determinístico que npm install)
  4. npm test — roda os testes

Faça commit e push. Vá na aba Actions do seu repositório — o workflow já vai estar rodando.

Step 2: Adicionar build

Depois dos testes passarem, queremos gerar o build de produção:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm test
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

O upload-artifact salva os arquivos gerados para uso em jobs posteriores ou para download manual.

Step 3: Deploy automatizado

Vamos deployar no Cloudflare Pages (que é o que o Decodifica.Tech usa). Adicionamos um job separado que depende do job de teste:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm test
      - run: npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci && npm run build
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy dist/ --project-name=meu-projeto

Pontos importantes:

  • needs: test — o deploy só roda se o job test passar
  • if: github.ref == 'refs/heads/main' — deploy apenas no branch main (PRs não deployam)
  • secrets.XXX — credenciais armazenadas de forma segura

Step 4: Configurar secrets

Secrets são variáveis criptografadas que o workflow usa sem expor no log. Para configurar:

  1. Vá em Settings → Secrets and variables → Actions
  2. Clique em New repository secret
  3. Adicione nome (ex: CLOUDFLARE_API_TOKEN) e valor
  4. No workflow, referencie com ${{ secrets.CLOUDFLARE_API_TOKEN }}

Secrets nunca aparecem nos logs — o GitHub automaticamente mascara qualquer tentativa de print.

Padrões avançados

Matrix builds — testar em múltiplas versões

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

Isso cria dois jobs em paralelo — um com Node 20 e outro com Node 22. Se o app funciona nas duas versões, você tem confiança de que não vai quebrar para ninguém.

Cache de dependências

O actions/setup-node com cache: 'npm' já cuida do cache automaticamente. Para projetos que precisam de cache customizado:

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

O cache reduz drasticamente o tempo de instalação de dependências — de minutos para segundos.

Concurrency groups — evitar deploys paralelos

Se dois pushes acontecem em sequência rápida, você não quer dois deploys rodando ao mesmo tempo:

concurrency:
  group: deploy-production
  cancel-in-progress: true

Isso garante que apenas o deploy mais recente execute. O anterior é cancelado automaticamente.

workflow_dispatch — trigger manual

Às vezes você quer rodar o pipeline manualmente (rollback, redeploy sem mudança de código):

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Ambiente de deploy'
        required: true
        default: 'production'
        type: choice
        options:
          - production
          - staging

Isso adiciona um botão “Run workflow” na aba Actions com um dropdown para escolher o ambiente.

Exemplo real: pipeline do Decodifica.Tech

O pipeline do Decodifica.Tech roda inteiramente no GitHub Actions. Todos os dias, um cron schedule dispara às 07:00 BRT e em cerca de 4 minutos:

  1. Coleta as notícias do dia (Python)
  2. Gera o conteúdo com IA
  3. Faz o build com Hugo
  4. Deploya no Cloudflare Pages

Tudo automático. Zero intervenção humana. Se algo falha, eu recebo uma notificação e posso investigar — mas na imensa maioria das vezes, funciona silenciosamente enquanto eu durmo.

Armadilhas comuns

Cron schedules podem atrasar ou ser pulados

O GitHub não garante execução exata do cron. Em momentos de alta carga, seu schedule pode atrasar 5-15 minutos ou até ser pulado. Se o horário exato é crítico, considere um trigger externo que faz workflow_dispatch via API.

Secrets não estão disponíveis em forks

Quando alguém abre um PR de um fork, os secrets do seu repositório não são acessíveis no workflow desse PR. Isso é uma proteção de segurança — impede que alguém modifique o workflow para exfiltrar suas credenciais. O job de deploy simplesmente não vai funcionar em PRs de forks.

Sempre fixe versões das actions

Use actions/checkout@v4, não actions/checkout@main. A tag @main pode mudar a qualquer momento e quebrar seu pipeline. Versões fixas garantem reprodutibilidade.

# ✅ Correto — versão fixada
- uses: actions/checkout@v4

# ❌ Arriscado — pode mudar sem aviso
- uses: actions/checkout@main

Para segurança máxima, você pode fixar pelo SHA do commit:

- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

Conclusão

GitHub Actions transformou a forma como faço deploy. E o melhor: é gratuito para a maioria dos projetos. Se quer ver um exemplo real completo, o workflow do Decodifica.Tech está aberto.

Comece simples — um workflow de testes. Depois adicione build. Depois deploy. Em uma tarde, você tem um pipeline que vai rodar sozinho pelos próximos meses sem precisar de manutenção.

A melhor parte? Uma vez que você configura CI/CD, nunca mais volta atrás. Deploy manual passa a parecer tão arcaico quanto copiar arquivos via FTP.