Preview environments: deploy automático de PRs para review

1. O que são Preview Environments e por que usá-las?

Preview environments (ou ambientes de pré-visualização) são ambientes efêmeros e isolados criados automaticamente a partir de um Pull Request (PR) no Git. Cada branch de feature, ao ter seu PR aberto, gera um ambiente completo onde a aplicação é deployada e pode ser acessada por revisores, testadores e stakeholders.

Os benefícios são diretos:
- Revisão visual: o revisor não precisa baixar o branch e rodar localmente; ele acessa uma URL e vê as mudanças em tempo real.
- Testes integrados: testes de integração e aceitação podem rodar contra o ambiente preview antes do merge.
- Feedback rápido: designers, product managers e QA podem interagir com a funcionalidade sem depender de desenvolvedores.
- Isolamento total: cada preview é independente; falhas em um não afetam outros ambientes nem a produção.

A relação com Git é intrínseca: cada branch de PR vira um ambiente isolado. O commit SHA, a branch e a URL do preview formam um vínculo rastreável dentro do repositório.

2. Estrutura de branches e gatilhos no Git

A estrutura básica de branches para preview environments segue este padrão:

main (ou develop)   → branch estável, base para produção
feature/*           → branches de funcionalidade, cada uma vira um preview

Os gatilhos no Git que disparam a criação e destruição dos ambientes são eventos do PR:

  • push: quando um novo commit é enviado para a branch do PR, o preview é atualizado.
  • pull_request (aberto): quando o PR é criado, o preview é gerado.
  • pull_request (sincronizado): quando o branch do PR recebe novos commits, o preview é recriado.
  • pull_request (reaberto): quando um PR fechado é reaberto, o preview é restaurado.
  • pull_request (fechado): quando o PR é mergeado ou fechado sem merge, o preview é destruído.

No GitHub Actions, esses gatilhos são configurados assim:

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

3. Configuração do deploy automático com Git e CI/CD

Vamos a um exemplo prático usando GitHub Actions. O workflow abaixo cria um preview environment para cada PR:

name: Deploy Preview

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout do branch do PR
        uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}

      - name: Build da aplicação
        run: |
          npm install
          npm run build

      - name: Deploy para preview
        run: |
          PR_NUMBER=${{ github.event.pull_request.number }}
          BRANCH_NAME=${{ github.head_ref }}
          COMMIT_SHA=${{ github.sha }}
          URL="https://pr-${PR_NUMBER}.meuapp.com"
          echo "Deployando preview para $URL"
          # Comando real de deploy (ex: AWS S3, Vercel, Netlify)
          ./deploy-preview.sh $PR_NUMBER $BRANCH_NAME $COMMIT_SHA

      - name: Comentar URL no PR
        uses: actions/github-script@v7
        with:
          script: |
            const url = `https://pr-${context.payload.pull_request.number}.meuapp.com`;
            github.rest.issues.createComment({
              issue_number: context.payload.pull_request.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `🔍 Preview disponível em: ${url}`
            });

O mapeamento entre commit SHA, branch e URL é essencial para rastreabilidade. Armazene esse mapeamento em um arquivo ou variável de ambiente:

# mapping.txt
pr-42,feature/login,abc123,https://pr-42.meuapp.com
pr-43,feature/logout,def456,https://pr-43.meuapp.com

4. Gerenciamento de ambientes com Git tags e commits

Tags temporárias ajudam a identificar versões do preview. A cada push no PR, a tag é atualizada:

# Criar ou atualizar tag para o preview
git tag -f preview/pr-42 ${{ github.sha }}
git push origin preview/pr-42 --force

Ao fechar o PR, a tag é removida:

# Remover tag do preview
git push origin --delete preview/pr-42

Isso mantém o repositório limpo e permite consultar rapidamente qual commit está em qual preview.

5. Estratégias de nomenclatura e roteamento

A nomenclatura deve ser única e previsível. Exemplos:

pr-42              → preview do PR número 42
feature-login      → baseado no nome do branch (substituindo / por -)

Subdomínios dinâmicos são a abordagem mais comum:

https://pr-42.meuapp.com
https://feature-login.meuapp.com

Para registrar URLs automaticamente, use hooks pós-recebe (post-receive) no servidor Git:

#!/bin/bash
# .git/hooks/post-receive
while read oldrev newrev refname
do
  if [[ $refname =~ refs/heads/feature/ ]]; then
    BRANCH=${refname#refs/heads/}
    PR_NUMBER=$(echo $BRANCH | grep -oP 'feature/\K\d+')
    URL="https://pr-${PR_NUMBER}.meuapp.com"
    echo "Preview URL: $URL"
    # Atualiza serviço de DNS ou roteador
  fi
done

6. Integração com revisão de código e testes

O link do preview deve aparecer automaticamente no comentário do PR. No GitHub Actions, usamos a API do GitHub:

- name: Comentar preview no PR
  uses: actions/github-script@v7
  with:
    script: |
      const url = `https://pr-${context.payload.pull_request.number}.meuapp.com`;
      github.rest.issues.createComment({
        issue_number: context.payload.pull_request.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `🔍 Preview: ${url}\n✅ Testes: ${process.env.TEST_STATUS}`
      });

Para bloquear o merge se o preview falhar, configure status checks no repositório Git:

# No GitHub, vá em Settings > Branches > Branch protection rules
# Marque "Require status checks to pass before merging"
# Adicione "preview-deploy" e "preview-tests" como checks obrigatórios

No workflow, o status check é atualizado automaticamente:

- name: Atualizar status check
  uses: actions/github-script@v7
  with:
    script: |
      github.rest.checks.create({
        owner: context.repo.owner,
        repo: context.repo.repo,
        name: 'preview-deploy',
        head_sha: context.sha,
        status: 'completed',
        conclusion: 'success' // ou 'failure'
      });

7. Limpeza e ciclo de vida dos ambientes

A remoção automática ao mergear ou fechar o PR é crítica para evitar acúmulo de recursos:

name: Cleanup Preview

on:
  pull_request:
    types: [closed]

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Remover preview
        run: |
          PR_NUMBER=${{ github.event.pull_request.number }}
          echo "Removendo preview do PR #$PR_NUMBER"
          # Comando real de remoção (ex: deletar bucket S3, remover subdomínio)
          ./destroy-preview.sh $PR_NUMBER

      - name: Remover tag Git
        run: |
          git push origin --delete preview/pr-${{ github.event.pull_request.number }} || true

Para ambientes abandonados (PRs esquecidos), implemente expiração por TTL:

# Script de cron que roda diariamente
for PR in $(list_all_previews); do
  CREATED_AT=$(get_preview_creation_time $PR)
  AGE_DAYS=$(( ( $(date +%s) - $CREATED_AT ) / 86400 ))
  if [ $AGE_DAYS -gt 7 ]; then
    echo "Removendo preview expirado: $PR"
    destroy_preview $PR
  fi
done

8. Boas práticas e armadilhas comuns

Evitar dados sensíveis: use variáveis de ambiente separadas para previews, nunca exponha credenciais de produção.

# .env.preview
DATABASE_URL=sqlite:///preview.db
API_KEY=preview-key-123

Gerenciar recursos: limite o número de ambientes simultâneos para evitar custos excessivos.

MAX_PREVIEWS=10
CURRENT_PREVIEWS=$(count_active_previews)
if [ $CURRENT_PREVIEWS -ge $MAX_PREVIEWS ]; then
  echo "Limite de previews atingido. Remova um preview existente."
  exit 1
fi

Versionamento de configurações: mantenha todos os scripts de deploy no repositório Git, versionados junto com o código.

repo/
├── .github/
│   └── workflows/
│       ├── deploy-preview.yml
│       └── cleanup-preview.yml
├── scripts/
│   ├── deploy-preview.sh
│   ├── destroy-preview.sh
│   └── list-previews.sh
└── .env.preview

Armadilhas comuns:

  • Cache de build: sempre faça builds limpos para cada preview, evitando cache de branches anteriores.
  • Concorrência: se dois pushes ocorrerem simultaneamente no mesmo PR, o último vence; implemente locks ou filas.
  • Recursos compartilhados: prefira bancos de dados efêmeros (ex: SQLite em memória) ou instâncias isoladas por preview.

Referências