Files
camilafagundesandClaude Opus 5 5b11496cd9
ci / lint, typecheck, testes e build (push) Has been cancelled
ci / Postgres e Redis reais (push) Has been cancelled
ci / imagem Docker Linux (push) Has been cancelled
Runbook único de homologação, checklist de evidências e ADR do ambiente
Havia dois runbooks sobrepostos, escritos para o piloto, e nenhum documento do
ambiente de homologação.

- `20-homologacao-runbook.md` consolida os dois e cobre DNS, TLS, Nginx,
  Supabase de homologação, banco com a decisão do shim, bucket privado,
  containers, worker, observabilidade, flag, rollback, diagnóstico e smoke.
  Os dois runbooks antigos ficam no repositório marcados como histórico,
  preservando o registro das decisões.
- Rollback de imagem com comandos: identificar o que está no ar, listar tags,
  desligar a flag antes de trocar, subir só web e worker com `--no-deps`,
  conferir saúde, religar a flag. As migrations de segurança — RLS e auditoria
  append-only — estão declaradas sem caminho de volta: desfazê-las afrouxa o
  isolamento entre workspaces, então um rollback que precise disso está errado
  por definição.
- `21-checklist-validacao.md` separa o que foi executado do que depende de
  infraestrutura, com a evidência de cada item e a tabela dos quatro defeitos
  encontrados. Nada está marcado PASS por inspeção de código.
- ADR 0007 registra o ambiente lógico e a autenticação de homologação, com as
  alternativas descartadas e o motivo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 20:40:08 -03:00

19 KiB

Runbook de homologação — hml-ada.elopro.app

Runbook único do Adaptador de Planilhas em homologação. Substitui 07-deployment-runbook.md e 16-pilot-deployment-runbook.md, que ficam no repositório como histórico e não devem ser seguidos.

Homologação hml-ada.elopro.app
Produção futura ada.elopro.app
Servidor compartilhado com os outros aplicativos do ecossistema
Projeto do compose adaptador-planilhas-hml
Rede Docker adaptador-planilhas-hml
Volume adaptador-planilhas-hml-redis
Portas publicadas só em 127.0.0.1: web 3110, Redis 56380
Namespace de chaves adaptador-planilhas:staging

Princípios que o procedimento respeita

  1. Nenhum serviço de outro aplicativo é criado, alterado ou reiniciado. O compose tem nome de projeto, rede e volume próprios, e o deploy usa up -d --no-deps web worker.
  2. DNS, serviços e banco de produção não são tocados. Homologação tem subdomínio, banco, bucket, Redis, projeto Supabase e projeto/ambiente Sentry próprios.
  3. Não há autenticação de desenvolvimento em homologação. ADAPTER_DEV_AUTH=1 faz o processo recusar o start fora de development.
  4. Nenhuma credencial administrativa é compartilhada entre aplicações ou entre ambientes.
  5. RLS e as validações multi-tenant não são afrouxadas em nenhum passo.
  6. Migrations destrutivas não rodam automaticamente. npm run migrate é um job separado, com aprovação manual, precedido de npm run migrate:check.
  7. Produção não é liberada sem smoke real e ensaio de rollback registrados em 21-checklist-validacao.md.

1. DNS

Registro novo, sem mexer em nada existente:

hml-ada.elopro.app.   A      <IP do servidor compartilhado>

Se o ecossistema usa CNAME para um host único, siga o mesmo padrão dos outros aplicativos. Não altere o registro de ada.elopro.app nem o apex elopro.app.

Certificado TLS: inclua apenas hml-ada.elopro.app na emissão. Com Certbot e Nginx:

sudo certbot --nginx -d hml-ada.elopro.app

Isso cria um bloco novo. Não use --expand em um certificado que atende aplicativos existentes: uma renovação com falha derrubaria o TLS deles.

2. Nginx

Arquivo novo em /etc/nginx/sites-available/hml-ada.elopro.app, sem editar nenhum server existente:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name hml-ada.elopro.app;

    ssl_certificate     /etc/letsencrypt/live/hml-ada.elopro.app/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/hml-ada.elopro.app/privkey.pem;

    # Upload de modelo e de fonte: o limite do produto é 10 MB por arquivo.
    # A margem cobre o envelope multipart.
    client_max_body_size 12m;

    # Processamento assíncrono responde rápido, mas a geração de PDF e XLSX
    # do download é síncrona. O teto do job é SPREADSHEET_JOB_TIMEOUT_MS.
    proxy_read_timeout 180s;
    proxy_send_timeout 180s;

    location / {
        proxy_pass http://127.0.0.1:3110;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";
    }

    # Sondas internas: o monitoramento usa estas duas rotas.
    location = /api/health/live  { proxy_pass http://127.0.0.1:3110; access_log off; }
    location = /api/health/ready { proxy_pass http://127.0.0.1:3110; }
}

server {
    listen 80;
    listen [::]:80;
    server_name hml-ada.elopro.app;
    return 301 https://$host$request_uri;
}

X-Forwarded-For é obrigatório: o rate limiting da tela de login identifica o chamador por ele.

Os cabeçalhos de segurança (CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy) vêm da aplicação, em next.config.ts. Não duplique no Nginx: add_header no servidor substituiria o conjunto da aplicação.

sudo ln -s /etc/nginx/sites-available/hml-ada.elopro.app /etc/nginx/sites-enabled/
sudo nginx -t          # precisa passar antes de recarregar
sudo systemctl reload nginx   # reload, nunca restart: não derruba os outros sites

3. Identidade (Supabase Auth de homologação)

O Adaptador não tem tabela de usuário, senha, cadastro ou sessão própria. Em homologação a identidade é um projeto Supabase separado, só de homologação.

  1. Crie o projeto Supabase de homologação. Anote URL, anon key e service role.
  2. A service role de homologação não é a de produção e não é compartilhada com nenhum outro aplicativo.
  3. Crie as contas de teste no painel do Supabase (Authentication → Users). Nenhuma senha passa pelo repositório, pelo seed ou por este runbook.
  4. Anote o UUID de cada conta criada: é o que liga a identidade ao workspace.
  5. Em .env.staging, deixe ADAPTER_STAGING_AUTH=1. Isso habilita /api/auth/staging/session, que faz signInWithPassword contra esse projeto. A sessão é emitida e validada pelo Supabase.
  6. Quando o SSO do Elo Pro existir, troque para ADAPTER_STAGING_AUTH=0 e ELO_PRO_LOGIN_URL=<login central>. Nada mais muda: o contrato está em src/server/auth/identity-provider.ts.

Workspace ativo

O workspace não vem de cookie como prova. O cookie elo_pro_workspace_id é preferência; toda requisição relê as associações reais em workspace_members sob RLS (src/server/auth/workspace.ts):

  • nenhuma associação → a pessoa vê "nenhum workspace";
  • uma associação → escolha automática;
  • várias → tela /selecionar-workspace, e POST /api/auth/workspace só grava o cookie depois de confirmar a associação.

Seed de homologação

cp config/staging-seed.example.json config/staging-seed.json
# preencha users[].id com os UUID do Supabase Auth e escolha os UUID de workspace
npm run seed:staging

Idempotente: rodar de novo produz o mesmo estado. Não cria conta nem senha, só liga identidades a workspaces e papéis. A feature flag nasce desligada mesmo aqui.

4. Banco de homologação

# 1. Veja o que vai rodar, antes de rodar.
ADAPTER_ENV=staging DATABASE_URL=... ADAPTER_APPLY_IDENTITY_SHIM=? npm run migrate:check

A decisão do shim é explícita e obrigatória: sem ADAPTER_APPLY_IDENTITY_SHIM o start é recusado.

Situação do banco Valor Efeito
Banco novo, sem profiles/workspaces/workspace_members 1 0001_platform_shim.sql cria o shim e o papel app_user
Banco já com o schema do Elo Pro 0 0001 é pulado; só 0002, 0003 e 0004 rodam

Com 0, antes de migrar: confirme que existem public.profiles(id, display_name, is_superadmin), public.workspaces(id, name) e public.workspace_members(workspace_id, user_id, role), e que o papel app_user existe sem BYPASSRLS. Se os nomes ou as colunas diferirem, adapte src/server/auth/identity-port.ts antes — é o único arquivo que precisa mudar.

# 2. Aplicar (job com aprovação manual no workflow).
npm run migrate

Nenhum processo aplica migration sozinho: o worker recusa iniciar com migration pendente e a web não migra em ambiente implantado.

5. Bucket privado

# Crie o bucket no painel do Supabase de homologação, com visibilidade privada.
# Nome sugerido: adaptador-planilhas-hml
npm run check:bucket

check:bucket falha se o bucket não existir ou for público. A aplicação também recusa o valor literal public em SUPABASE_STORAGE_BUCKET. Nenhuma URL pública é gerada: o download passa por /api/spreadsheet-adapter/jobs/[id]/download, autenticado e sob RLS.

6. Containers

A imagem tem um entrypoint com subcomandos. Use sempre o subcomando, nunca node direto: ele garante node --import tsx, que é o que faz o SIGTERM do docker stop chegar ao processo do worker. O CLI do tsx cria um processo filho e o sinal pararia nele.

cd <HML_DEPLOY_PATH>
cp .env.staging.example .env.staging
# preencha todos os campos e confira
export C="docker compose -f docker-compose.staging.yml --env-file .env.staging"
$C run --rm web verify-config

$C up -d redis
$C up -d --no-deps web worker
$C ps
Subcomando O que faz
web servidor Next (saída standalone)
worker processo de fila
migrate [--dry-run] migrations
seed-staging associa identidades do Supabase a workspaces
flag <workspace_id> on off feature flag do módulo
cleanup expira temporários fora do ciclo do worker
verify-config confere a configuração sem tocar em recurso externo
check-bucket confere que o bucket existe e é privado
smoke smoke do ambiente
worker-health confere o heartbeat do worker

--no-deps e o --project-name embutido (adaptador-planilhas-hml) garantem que nenhum stack de outro aplicativo é tocado. Nunca rode docker compose down sem -f docker-compose.staging.yml, e nunca docker system prune neste servidor.

Web e worker saem da mesma imagem: mesmo código, mesmas dependências. A diferença é só o subcomando. O node_modules de produção e o traçado pelo standalone vêm do mesmo lockfile e não divergem em nenhuma versão; a imagem sobrepõe as duas árvores nessa ordem, com a traçada por último, que é a que o server.js espera.

O Redis é um contêiner exclusivo deste aplicativo, com senha, volume e porta próprios. Mesmo assim as chaves são prefixadas com adaptador-planilhas:staging, então um Redis compartilhado por engano não misturaria homologação com produção.

7. Worker

$C logs -f worker
$C exec worker /app/docker/entrypoint.sh worker-health

O start imprime [worker] pronto. intervalo=…ms lote=… carencia=…ms alvo=postgres heartbeat=….

  • ADAPTER_JOB_MODE=worker é obrigatório. inline faz o start ser recusado.
  • Migration pendente faz o worker sair com código 1, com a lista no log.
  • SIGTERM espera o tick corrente (até ADAPTER_WORKER_GRACE_MS, padrão 20 s, dentro do stop_grace_period de 30 s) para o finally liberar o lock. Sem isso o job ficaria em processing até o lock vencer por TTL. O log de uma parada limpa traz SIGTERM recebido e [worker] encerrado; se faltarem, o contêiner levou SIGKILL e o lock vai esperar o TTL de 120 s.
  • Um job que estava apenas na fila, e não começou, permanece queued: nada pela metade. O worker seguinte o conclui.
  • O worker grava um heartbeat ao fim de cada tick (ADAPTER_WORKER_HEARTBEAT, padrão /tmp/adaptador-worker-heartbeat). O healthcheck do contêiner usa esse arquivo, porque um processo vivo mas travado passa em qualquer verificação de PID.
  • Cada tick também expira temporários (cleanupExpired) e recupera jobs órfãos (recoverStaleJobs). Não há cron separado.
  • O log traz nome e código do erro. Nunca conteúdo de célula.

8. Observabilidade

Item Onde
Ambiente no Sentry SENTRY_ENVIRONMENT=staging; sem a variável, cai em ADAPTER_ENV
Release SENTRY_RELEASE = SHA do commit, injetado como build-arg da imagem
Sourcemap withSentryConfig só atua com SENTRY_ORG, SENTRY_PROJECT e SENTRY_AUTH_TOKEN no build
Erros de rota e render onRequestError em src/instrumentation.ts
Sanitização scrubSentryEvent descarta texto livre, cookie, header de autorização e qualquer campo de célula
Liveness GET /api/health/live — não toca dependência
Readiness GET /api/health/ready — banco, migrations pendentes, Redis (PING), bucket (listagem), configuração e modo de job
Auditoria public.audit_log, append-only para app_user, sem conteúdo de célula

Teste a sanitização provocando um erro controlado e confirmando no Sentry que o evento não traz célula, cookie nem nome de pessoa.

9. Liberação da feature flag

Último passo, depois do smoke:

npm run flag -- <workspace_id> on

O script recusa um workspace que não existe e registra o evento em audit_log. A flag nasce desligada, inclusive em homologação.

10. Rollback

Aplicação: rollback de imagem (reversível, é o caminho normal)

O artefato é imutável e marcado com o SHA do commit: ghcr.io/<org>/<repo>:staging-<sha>. Voltar é apontar o compose para a tag anterior. Nada de banco, bucket ou auditoria é tocado.

export C="docker compose -f docker-compose.staging.yml --env-file .env.staging"

# 1. Qual imagem está no ar agora. Anote: é o ponto de retorno se o rollback falhar.
$C ps --format '{{.Service}} {{.Image}}'
docker inspect "$($C ps -q web)" --format '{{.Config.Image}}'

# 2. Quais tags existem para voltar (mais recentes primeiro).
docker image ls 'ghcr.io/<org>/<repo>' --format '{{.Tag}}	{{.CreatedAt}}' | sort -k2 -r | head

# 3. Desligar o módulo para os usuários. Efeito imediato, sem deploy.
#    Faça isto ANTES de trocar a imagem: encerra o incidente de produto em segundos.
$C run --rm web flag <workspace_id> off

# 4. Trocar para a imagem anterior. Só web e worker; o Redis fica de pé.
export ADAPTER_IMAGE=ghcr.io/<org>/<repo>:staging-<sha-anterior>
docker pull "$ADAPTER_IMAGE"
$C up -d --no-deps web worker

# 5. Conferir que voltou e está saudável.
docker inspect "$($C ps -q web)" --format '{{.Config.Image}}'
curl -fsS https://hml-ada.elopro.app/api/health/live
curl -fsS https://hml-ada.elopro.app/api/health/ready
$C exec worker /app/docker/entrypoint.sh worker-health

# 6. Religar a flag só depois de confirmar o ambiente.
$C run --rm web flag <workspace_id> on

Para parar apenas o processamento, sem derrubar a interface:

$C stop -t 30 worker      # respeita a carência: o lock é liberado

Jobs queued param de avançar e são retomados quando o worker volta. Jobs processing sem lock viram INTERRUPTED na próxima varredura e podem ser repetidos pelo usuário. Nenhuma saída já gerada é apagada: ela vence pelo TTL.

Fixe a tag no .env.staging (ADAPTER_IMAGE=...:staging-<sha>) para que um up -d acidental não traga de volta a imagem com defeito.

Migrations não voltam no rollback de imagem. As migrations do Adaptador são aditivas: a imagem anterior convive com o schema novo. Se a imagem anterior não conviver, o problema é de compatibilidade de schema e vira operação planejada, não rollback às pressas.

Schema (não automatizado, de propósito)

Não existe rollback automático de schema e não deve existir.

Migration Caminho de volta
0002_spreadsheet_adapter.sql migrations/down/0002_spreadsheet_adapter_down.sql, aplicado à mão, com backup antes
0003_rls.sql sem down. Remover policy é afrouxar isolamento: não se desfaz, corrige-se para frente
0004_audit_append_only.sql sem down. A trava de auditoria não se remove em rollback
0001_platform_shim.sql sem down. O shim só existe em banco que não tem o schema do Elo Pro

Regra: desligar a flag resolve o incidente de produto. Reverter imagem resolve o incidente de código. Mexer em schema é operação planejada com backup, nunca parte de um rollback às pressas.

Migrations de segurança são preservadas em qualquer rollback. 0003_rls.sql (RLS e FORCE RLS) e 0004_audit_append_only.sql (auditoria append-only) não têm e não terão caminho de volta: desfazê-las afrouxa isolamento entre workspaces e abre a auditoria para alteração pelo papel da aplicação. Um rollback que precise disso está errado por definição.

Não apague audit_log, modelos, banco ou arquivos em nenhum rollback.

11. Diagnóstico

Sintoma Causa provável Verificação
Container sai no start com código 1 configuração recusada docker compose logs web traz a lista de variáveis
/api/health/live não responde processo caiu configuração é a primeira hipótese: veja o log do start
ready com config: missing variável ausente ou inválida configIssues na resposta nomeia cada uma
ready com redis: down senha, host ou contêiner docker compose ps redis e o PING
ready com storage: down bucket, nome ou service role npm run check:bucket
ready com migrations: missing migration pendente npm run migrate:check
ready com job_mode: missing ADAPTER_JOB_MODE=inline corrija e suba de novo
Login cai em branco ADAPTER_STAGING_AUTH desligado e sem ELO_PRO_LOGIN_URL /login explica qual falta
Fica pedindo workspace identidade sem associação npm run seed:staging ou incluir em workspace_members
Job preso em processing worker parado ou lock vencido log do worker; a varredura marca INTERRUPTED
429 nas chamadas rate limiting Retry-After na resposta; tetos em src/server/rate-limit.ts

12. Ordem completa, do zero

  1. DNS de hml-ada.elopro.app.
  2. Certificado TLS só desse host.
  3. server novo no Nginx, nginx -t, systemctl reload nginx.
  4. Projeto Supabase de homologação; contas de teste no painel.
  5. Bucket privado; npm run check:bucket.
  6. .env.staging preenchido; npm run verify:config.
  7. npm run migrate:check e depois npm run migrate (aprovação manual).
  8. config/staging-seed.json e npm run seed:staging.
  9. up -d redis, depois up -d --no-deps web worker.
  10. /api/health/live responde live; /api/health/ready responde ready.
  11. Smoke, nas duas fases (ver seção 13).
  12. Roteiro manual de 21-checklist-validacao.md: login real, isolamento, processamento assíncrono, download XLSX/CSV/PDF, erro no Sentry.
  13. npm run flag -- <workspace_id> on.
  14. Ensaio de rollback, com evidência.
  15. Só então discutir produção.

13. Smoke de homologação

Fase 1 roda sem nenhuma credencial: saúde, recusa sem sessão, cabeçalhos de segurança e ausência de dev auth.

ADAPTER_SMOKE_URL=https://hml-ada.elopro.app npm run smoke:staging

Fase 2 só roda quando a operação fornece uma conta de teste criada no painel do Supabase de homologação. Nenhuma credencial fica no repositório, e o script não inventa conta: sem estas variáveis a fase 2 é reportada como PENDENTE, nunca como aprovada.

Variável Obrigatória O que é
ADAPTER_SMOKE_URL sim https://hml-ada.elopro.app
ADAPTER_SMOKE_EMAIL fase 2 e-mail de uma conta de teste do Supabase de homologação
ADAPTER_SMOKE_PASSWORD fase 2 senha dessa conta
ADAPTER_SMOKE_WORKSPACE opcional UUID do workspace que deve aparecer associado
ADAPTER_SMOKE_URL=https://hml-ada.elopro.app ADAPTER_SMOKE_EMAIL=... ADAPTER_SMOKE_PASSWORD=... ADAPTER_SMOKE_WORKSPACE=...   npm run smoke:staging

A fase 2 confere: login real emitindo sessão, associações de workspace resolvidas no servidor, seleção de workspace aceita e leitura autenticada do módulo. Com a feature flag ainda desligada, FEATURE_DISABLED é resposta correta e passa.

Continua fora do smoke, no roteiro manual, porque exige arquivo e flag ligada:

  • envio de modelo e de fonte pela interface;
  • processamento assíncrono com o worker e download XLSX, CSV e PDF;
  • troca de workspace e sessão expirada no navegador;
  • erro controlado no Sentry, confirmado sem conteúdo de célula;
  • ensaio de rollback de imagem.