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>
This commit is contained in:
1 parent
a1ceef6c5f
commit
5b11496cd9
6 files changed
+644
-7
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# Deploy e operação
|
||||
|
||||
> **Histórico.** Este runbook foi escrito para o piloto, antes do ambiente de
|
||||
> homologação existir. Não siga este arquivo. O procedimento vigente está em
|
||||
> [`20-homologacao-runbook.md`](20-homologacao-runbook.md), que consolida este e
|
||||
> o `16-pilot-deployment-runbook.md`. Mantido para registro das decisões.
|
||||
|
||||
|
||||
## Ordem
|
||||
|
||||
1. Conferir envs sem colar segredo no repositório.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Runbook de implantação do piloto
|
||||
|
||||
> **Histórico.** Este runbook foi escrito para o piloto, antes do ambiente de
|
||||
> homologação existir. Não siga este arquivo. O procedimento vigente está em
|
||||
> [`20-homologacao-runbook.md`](20-homologacao-runbook.md), que consolida este e
|
||||
> o `16-pilot-deployment-runbook.md`. Mantido para registro das decisões.
|
||||
|
||||
|
||||
Execute nesta ordem. Não apague auditoria, banco ou arquivos como parte do rollback.
|
||||
|
||||
1. Preparar o projeto Supabase da Elo Pro e anotar URL, anon key e service role.
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```nginx
|
||||
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.
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
$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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
$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.
|
||||
|
||||
```bash
|
||||
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 |
|
||||
|
||||
```bash
|
||||
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.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Checklist de validação e evidências — homologação
|
||||
|
||||
Estado em 15 de outubro de 2026, nesta máquina de desenvolvimento, sem acesso ao servidor do ecossistema, ao Supabase de homologação nem ao banco do Elo Pro.
|
||||
|
||||
**Sem runtime de contêiner nesta máquina.** O Docker Desktop está instalado e foi iniciado, mas o WSL2 não tem distribuição instalada (`wsl --list` reporta nenhuma), então o engine Linux não sobe. Não há Postgres, Redis, Podman nem nerdctl locais. O que exige contêiner ou serviço real foi validado por **simulação fiel do layout de runtime da imagem** (apenas `node_modules` de produção + `src` + `scripts` + `migrations` + `tsconfig.json`, executado com `node --import tsx`) e, onde nem isso alcança, está PENDENTE para o CI.
|
||||
|
||||
Regra de preenchimento: **PASS** só com evidência executada. Sem infraestrutura ou credencial, o item é **PENDENTE** — nunca aprovado por inspeção de código.
|
||||
|
||||
## A. Verificação local do repositório
|
||||
|
||||
| # | Item | Resultado | Evidência |
|
||||
| --- | --- | --- | --- |
|
||||
| A1 | `npm run lint` | PASS | sem apontamentos |
|
||||
| A2 | `npm run typecheck` | PASS | `tsc --noEmit` sem erro |
|
||||
| A3 | `npm test` | PASS | 17 arquivos, **102 testes**, 1 skip proposital (smoke real) |
|
||||
| A4 | `npm run build` | PASS | 25 rotas + Proxy, zero avisos, saída `standalone` com 70 MB contendo só `.next`, `node_modules`, `migrations`, `src`, `package.json` e `server.js` |
|
||||
| A5 | Testes HTTP das rotas (novos) | PASS | `tests/http/routes.test.ts`, 12 casos |
|
||||
| A6 | Testes do proxy (novos) | PASS | `tests/http/proxy.test.ts`, 7 casos |
|
||||
| A7 | Política de ambiente e workspace (novos) | PASS | `tests/unit/staging-policy.test.ts`, 22 casos |
|
||||
| A8 | Suíte com Postgres e Redis reais | PENDENTE | `npm run test:services` reporta 15 ignorados sem `ADAPTER_TEST_DATABASE_URL`/`ADAPTER_TEST_REDIS_URL`. O CI executa no job `services` |
|
||||
| A9 | Sintaxe do compose local | PASS | `docker compose -f docker-compose.yml config` sem erro |
|
||||
| A10 | Sintaxe do compose de homologação | PASS | `docker compose -f docker-compose.staging.yml config` resolve todas as variáveis |
|
||||
| A11 | Build da imagem Docker | PENDENTE | Docker Desktop está instalado nesta máquina, foi iniciado durante o trabalho e o engine Linux não chegou a responder (`500` no pipe `dockerDesktopLinuxEngine`). O CI executa no job `image` |
|
||||
| A12 | `docker compose up` local | PENDENTE | mesmo motivo de A11 |
|
||||
| A13 | Artefato `standalone` roda de verdade | PASS | `node .next/standalone/server.js` sobe, responde e atende as rotas (ver B6, C14, D3) |
|
||||
| A14 | Deps de produção cobrem a árvore traçada | PASS | `npm ci --omit=dev` em diretório limpo: 318 pacotes contra 153 traçados, **zero divergência de versão** entre os compartilhados; 3 pacotes existem só na traçada (`@emnapi/runtime`, `isarray`, `supports-color`) e o `COPY` do Docker mescla sem removê-los |
|
||||
| A15 | Worker e scripts rodam sem devDependencies | PASS | os 13 pacotes externos usados por `src/` e `scripts/` existem no install de produção; `tsx` 4.23.15 e `esbuild` 0.28.2 presentes |
|
||||
| A16 | Scripts operacionais executam no layout de runtime | PASS | no layout simulado: `verify-config`, `migrate`, `migrate --dry-run`, `seed-dev`, `cleanup-temp`, `set-flag` (recusa workspace inexistente), `seed-staging` (recusa em development) — todos com o código de saída correto |
|
||||
|
||||
## B. Fail-fast de configuração
|
||||
|
||||
| # | Item | Resultado | Evidência |
|
||||
| --- | --- | --- | --- |
|
||||
| B1 | Configuração incompleta é recusada | PASS | `tests/unit/staging-policy.test.ts`: `assertDeployedEnv` lista `DATABASE_URL` e `REDIS_URL` ausentes |
|
||||
| B2 | `ADAPTER_DEV_AUTH=1` recusado fora de development | PASS | teste de política e `devAuthEnabled()` |
|
||||
| B3 | `ADAPTER_JOB_MODE=inline` recusado em staging e produção | PASS | teste de política; `jobRunsInline` retorna `false` |
|
||||
| B4 | Shim de identidade exige decisão explícita | PASS | ausência de `ADAPTER_APPLY_IDENTITY_SHIM` entra em `configIssues` |
|
||||
| B5 | Sem `DATABASE_URL` nunca cai em PGlite | PASS | `requireDatabaseUrl` lança em staging e produção |
|
||||
| B6 | O processo **sai** com código 1, em vez de responder 500 | PASS | executado no artefato construído: `ADAPTER_ENV=staging node .next/standalone/server.js` imprimiu a lista de variáveis, saiu com **código 1** e a porta ficou sem nada ouvindo. Antes desta entrega o mesmo comando deixava o servidor no ar respondendo 500 em todas as rotas, inclusive `/api/health/live` |
|
||||
| B7 | `npm run verify:config` recusa e explica | PASS | em `ADAPTER_ENV=staging` sem variáveis: 9 itens nomeados, saída com código 1 |
|
||||
| B8 | O worker também recusa configuração ausente | PASS | no layout de runtime, `ADAPTER_ENV=staging` sem variáveis: saída com código 1 antes de tocar banco ou Redis |
|
||||
| B9 | Contêiner da web e do worker saem com código ≠ 0 | PENDENTE | passo `Configuração ausente encerra com código diferente de zero` do job `image`, que inspeciona `.State.ExitCode` dos dois |
|
||||
|
||||
## C. Autenticação e multi-tenant
|
||||
|
||||
| # | Item | Resultado | Evidência |
|
||||
| --- | --- | --- | --- |
|
||||
| C1 | API do módulo sem sessão responde 401 | PASS | `tests/http/routes.test.ts` |
|
||||
| C2 | Página protegida sem sessão vai ao login | PASS | `tests/http/proxy.test.ts` |
|
||||
| C3 | Cookie de workspace não é prova de autorização | PASS | `resolveWorkspace` ignora dica sem associação real; teste com dica forjada e com injeção |
|
||||
| C4 | Gravar workspace de terceiro é recusado | PASS | `POST /api/auth/workspace` → 403 e cookie não gravado |
|
||||
| C5 | Associações listadas são só as do usuário | PASS | teste com Eva, que só vê Beta |
|
||||
| C6 | Módulo desligado bloqueia o workspace | PASS | teste com Diego em Beta |
|
||||
| C7 | Papel sem permissão não altera configuração | PASS | teste com Bruno (agent) |
|
||||
| C8 | Login de homologação indisponível fora de staging | PASS | `/api/auth/staging/session` → 404 |
|
||||
| C16 | Dev auth não liga em homologação, em nenhuma combinação | PASS | `tests/unit/dev-auth-guard.test.ts`, 18 casos: matriz de ambientes na função, recusa na validação de configuração, e o provedor local fora da lista de provedores em staging e produção. Um token de dev válido é recusado quando o ambiente é staging |
|
||||
| C9 | Isolamento entre workspaces na suíte de aplicação | PASS | `tests/security/isolation.test.ts`, 6 casos |
|
||||
| C10 | RLS em PostgreSQL real, papel `app_user` sem BYPASSRLS | PENDENTE | `tests/services/postgres.test.ts` escrito; roda no CI e no banco de homologação |
|
||||
| C11 | Login real no navegador, com conta do Supabase de homologação | PENDENTE | exige o projeto Supabase de homologação. O caminho equivalente foi executado no nível de development (C14) |
|
||||
| C14 | Login real e isolamento no artefato em execução | PASS | contra `node server.js`: sessão emitida, `GET /api/auth/workspace` devolveu só o workspace da identidade, `POST` com workspace de terceiro → **403** sem gravar cookie, `POST` com o próprio → 200, `/adaptador` → 200 |
|
||||
| C15 | Cookie de workspace forjado é ignorado em execução | PASS | três tentativas com a sessão de Ana (Alpha): cookie apontando Beta, cookie com `' or 1=1 --` e cookie com UUID inexistente. Em todas a resolução permaneceu em Alpha; Beta nunca foi alcançado |
|
||||
| C12 | Troca de workspace na interface | PENDENTE | exige duas associações reais em homologação |
|
||||
| C13 | Sessão expirada volta ao login | PENDENTE | exige sessão real |
|
||||
|
||||
## D. Infraestrutura e processamento
|
||||
|
||||
| # | Item | Resultado | Evidência |
|
||||
| --- | --- | --- | --- |
|
||||
| D1 | Rate limiting devolve 429 com `Retry-After` | PASS | `tests/http/routes.test.ts` |
|
||||
| D2 | Cabeçalhos de segurança configurados | PASS | `next.config.ts`: CSP, HSTS, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, COOP |
|
||||
| D18 | Smoke prepara a fase autenticada do Supabase | PASS no script / PENDENTE na execução | `scripts/smoke-staging.ts` tem as duas fases; sem `ADAPTER_SMOKE_EMAIL`/`PASSWORD` a fase 2 imprime PENDENTE. Credenciais listadas na seção 13 do runbook |
|
||||
| D3 | Cabeçalhos presentes na resposta HTTP real | PASS no artefato / PENDENTE atrás do Nginx | resposta de `/api/health/live` trouxe CSP, HSTS, `X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy`, COOP, `X-Content-Type-Options` e `Cache-Control: no-store`. Falta confirmar que o Nginx não sobrescreve |
|
||||
| D4 | Redis e Supabase reutilizados por processo | PASS | singleton em `src/infra/redis/index.ts` e `src/infra/storage/index.ts` |
|
||||
| D5 | Namespace separado por aplicativo e ambiente | PASS | `resourceNamespace`; teste confirma que staging ≠ production |
|
||||
| D6 | Listagem de bucket paginada | PASS | `src/infra/storage/supabase.ts` pagina com `offset` e desce em subpastas |
|
||||
| D7 | Bucket privado conferido | PENDENTE | `npm run check:bucket` exige o bucket real |
|
||||
| D8 | Lock entre processos com Redis real | PENDENTE | `tests/services/redis.test.ts`; roda no CI |
|
||||
| D9 | Processamento assíncrono entre processos | PENDENTE | `.github/scripts/async-journey.sh` percorre web → Redis → worker → download no compose; roda no job `image` |
|
||||
| D9b | Jornada assíncrona pelo loop real do worker | PASS | `tests/integration/worker.test.ts`: a web enfileira e devolve `queued`, o download falha antes do tick, o tick conclui e os três formatos saem; o modelo original não muda |
|
||||
| D9c | Job sobrevive à perda da fila do Redis | PASS | fila drenada à mão: a varredura do Postgres acha o job `queued` e conclui |
|
||||
| D9d | Worker relê o contexto do dono | PASS | flag do módulo desligada entre o enfileiramento e o tick: o job não é processado |
|
||||
| D10 | Worker recusa iniciar com migration pendente | PASS por construção | `pendingMigrations` no start; a execução real é PENDENTE |
|
||||
| D11 | Desligamento gracioso do worker | PASS no loop / PENDENTE no sinal | `shutdown()` para de aceitar trabalho, espera o tick corrente sair, e o job que não começou fica `queued` com o lock reservado para ele; concluído, o lock é liberado. A **entrega do SIGTERM** ao processo é PENDENTE: no Windows não há sinal real, e o job `image` do CI confere `SIGTERM recebido`, `[worker] encerrado` e exit 0 em menos de 25 s |
|
||||
| D17 | Heartbeat do worker e healthcheck | PASS | worker no layout de runtime gravou o heartbeat; `docker/worker-health.mjs` aprovou com 1 s e reprova quando o arquivo falta |
|
||||
| D12 | Download XLSX, CSV e PDF ponta a ponta | PASS no local / PENDENTE no ambiente | `tests/integration/journey.test.ts` cobre o fluxo; falta a jornada em homologação |
|
||||
| D13 | Modelo original não é alterado | PASS | `tests/unit/xlsx-fixtures.test.ts` compara bytes |
|
||||
| D14 | TTL de temporários aplicado | PASS no local / PENDENTE no ambiente | `tests/integration/journey.test.ts`; falta observar o vencimento real |
|
||||
| D15 | Health check responde `ready` com dependências satisfeitas | PASS | primeira chamada com banco frio: `{"status":"ready"}` com `config`, `database`, `migrations`, `redis`, `storage` e `job_mode` em `ok`, sem connection string na resposta |
|
||||
| D16 | Health check aponta a dependência que falhou | PASS | campo `reasons` traz só o nome do erro e os ids de migration pendente |
|
||||
|
||||
## E. Observabilidade
|
||||
|
||||
| # | Item | Resultado | Evidência |
|
||||
| --- | --- | --- | --- |
|
||||
| E1 | Payload do Sentry sem conteúdo de célula | PASS | `tests/unit/pilot-policy.test.ts` com sentinela |
|
||||
| E2 | Ambiente e release por ambiente | PASS | `sentryInitOptions` usa `ADAPTER_ENV` e `SENTRY_RELEASE` |
|
||||
| E3 | `onRequestError` registrado | PASS | export em `src/instrumentation.ts` |
|
||||
| E4 | Evento real chega ao projeto Sentry de homologação | PENDENTE | exige DSN real |
|
||||
| E5 | Sourcemap com stack legível | PENDENTE | exige `SENTRY_ORG`, `SENTRY_PROJECT` e `SENTRY_AUTH_TOKEN` no build |
|
||||
| E6 | Auditoria sem conteúdo de célula | PASS | `tests/integration/rls.test.ts` e `sanitizeAuditMetadata` |
|
||||
|
||||
## F. Ambiente e deploy
|
||||
|
||||
| # | Item | Resultado | Evidência |
|
||||
| --- | --- | --- | --- |
|
||||
| F1 | Dockerfile multistage com `standalone` | PASS | `Dockerfile` + `output: "standalone"` |
|
||||
| F2 | Scripts operacionais funcionam sem devDependencies | PASS | executados no layout de runtime simulado (ver A15 e A16), não apenas por construção |
|
||||
| F12 | Job de build Docker Linux no CI com validação da imagem | PASS no workflow / PENDENTE na execução | job `image`: build, conteúdo, usuário não-root, ausência de segredo/docs/testes, carga dos scripts, fail-fast dos dois processos, liveness com cabeçalhos, compose completo, jornada assíncrona entre processos e desligamento gracioso |
|
||||
| F13 | Rollback de imagem documentado | PASS | runbook seção 10: identificar a imagem no ar, listar tags, desligar a flag antes, trocar só web e worker com `--no-deps`, conferir saúde, religar a flag. `0003_rls.sql` e `0004_audit_append_only.sql` declaradas sem caminho de volta |
|
||||
| F3 | Node e gerenciador fixados | PASS | `.nvmrc` 24.11.1, `engines.node >=22.12 <25`, `packageManager npm@11.6.2` |
|
||||
| F4 | Compose isolado dos outros aplicativos | PASS | nome de projeto, rede e volume próprios; portas só em `127.0.0.1`; deploy com `--no-deps` |
|
||||
| F5 | `.env.staging.example` sem segredo | PASS | todos os campos sensíveis vazios |
|
||||
| F6 | Migration, deploy e flag em jobs separados com aprovação | PASS | `.github/workflows/deploy-staging.yml`, ambientes `hml-migracao`, `hml-deploy`, `hml-flag` |
|
||||
| F7 | Deploy real em `hml-ada.elopro.app` | PENDENTE | exige servidor, DNS e segredos |
|
||||
| F8 | DNS e TLS do subdomínio | PENDENTE | exige acesso ao DNS |
|
||||
| F9 | Nginx do novo `server` | PENDENTE | exige acesso ao servidor |
|
||||
| F10 | Smoke de homologação | PENDENTE | `npm run smoke:staging` exige o ambiente |
|
||||
| F11 | Ensaio de rollback | PENDENTE | exige duas imagens publicadas e o ambiente |
|
||||
|
||||
## Resumo
|
||||
|
||||
| | Quantidade |
|
||||
| --- | --- |
|
||||
| PASS com evidência executada | 54 |
|
||||
| PENDENTE por falta de infraestrutura ou credencial | 20 |
|
||||
| FAIL | 0 |
|
||||
|
||||
Nada foi marcado PASS por inspeção de código. Os PENDENTE que dependem só de Docker (A11, A12, A8, B9, C10, D8, D9, F12) executam no CI no primeiro push; os demais dependem de infraestrutura do ecossistema.
|
||||
|
||||
## Defeitos encontrados nesta rodada de validação
|
||||
|
||||
Os quatro apareceram justamente ao exercitar o layout de runtime da imagem, e nenhum aparecia na suíte de testes ou no build.
|
||||
|
||||
| # | Defeito | Efeito em homologação | Correção |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | O CLI do `tsx` cria processo filho | SIGTERM do `docker stop` pararia no pai; o worker nunca rodaria o handler, o lock esperaria o TTL de 120 s e o `stop_grace_period` só atrasaria um SIGKILL | entrypoint usa `node --import tsx`; PID conferido igual |
|
||||
| 2 | Top-level await nos scripts com `--import tsx` | `migrate`, `worker`, `seed-staging`, `set-flag`, `cleanup` e `check-bucket` falhariam com `Transform failed`. O worker não subiria e a migração não rodaria | `runScript`/`runWithDb` em `scripts/_bootstrap.ts`; nenhum script usa top-level await |
|
||||
| 3 | `import "server-only"` em `worker-loop.ts` | o pacote lança fora do bundler do Next: o worker morreria no start | removido do módulo compartilhado, com o motivo registrado no arquivo |
|
||||
| 4 | `@react-pdf/renderer` carregado no import de `process-job.ts` | `ERR_PACKAGE_PATH_NOT_EXPORTED` ao resolver a hifenização fora do bundler: derrubava o worker e o `cleanup` | import tardio dentro do ramo de PDF, que só o download usa |
|
||||
|
||||
## Bloqueios para liberar homologação
|
||||
|
||||
Todos externos ao repositório. Nenhum depende de código.
|
||||
|
||||
1. DNS de `hml-ada.elopro.app` e certificado TLS.
|
||||
2. Projeto Supabase de homologação, com contas de teste e bucket privado.
|
||||
3. Banco de homologação com `DATABASE_URL` e a decisão do shim conferida.
|
||||
4. Servidor com Docker, `HML_DEPLOY_PATH` e os segredos dos ambientes do GitHub.
|
||||
5. DSN do Sentry de homologação.
|
||||
|
||||
## Bloqueios para discutir produção
|
||||
|
||||
Além de tudo acima, concluído:
|
||||
|
||||
1. Smoke real completo, com evidência, em homologação.
|
||||
2. Ensaio de rollback executado, com evidência.
|
||||
3. SSO do Elo Pro integrado e validado no navegador.
|
||||
4. Schema real do Elo Pro confirmado e `identity-port.ts` apontando para ele.
|
||||
5. Tokens oficiais do Design System. O azul `#0B3D91` continua provisório.
|
||||
|
||||
## Evidências a anexar
|
||||
|
||||
Para cada item PENDENTE que for executado, registre aqui: data, quem executou, comando ou tela, e saída obtida. Resposta de `GET /api/health/ready` (sem segredo), log do worker concluindo um job, print do download dos três formatos, print do evento sanitizado no Sentry e log do ensaio de rollback.
|
||||
Reference in new issue
Block a user