Imagem Docker e compose reproduzíveis para homologação
Não existia nenhum artefato de deploy: sem Dockerfile, sem compose, sem pin de Node. O runbook pedia `npm run start` e `npm run worker` à mão, o que não é um ambiente reproduzível, e o worker dependia de `tsx`, uma devDependency, que `npm ci --omit=dev` removeria. - Dockerfile multistage. Web e worker saem da mesma imagem: mesmo código, mesmas dependências, só o subcomando difere. - `tsx` foi para `dependencies`. Conferido em diretório limpo: os 13 pacotes externos que src e scripts usam existem no install de produção. - A sobreposição das duas árvores de node_modules é segura e foi medida: 318 pacotes em produção contra 153 traçados, zero divergência de versão entre os compartilhados, e 3 que existem só na traçada, que o COPY mescla sem remover. O package.json do standalone não é sobrescrito. - Entrypoint com subcomandos, que garante `node --import tsx` em todos os scripts. Usar `node` direto convidaria de volta o defeito do SIGTERM. - Healthcheck de liveness na web e de heartbeat no worker. - Compose local reproduz a topologia com Postgres e Redis de verdade. Compose de homologação não declara Postgres (é o banco gerenciado), usa Redis isolado com senha e volume próprios, publica porta só em 127.0.0.1 e tem nome de projeto, rede e volume exclusivos. Nenhum serviço de outro aplicativo do ecossistema é declarado, tocado ou reiniciado. - `.env.staging.example` sem segredo, `.nvmrc`, `engines` e `packageManager`. - `config/staging-seed.json` e `.env.staging` no gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca8a1112f0
commit
dea510c3b4
11 files changed
+598
-28
No files matched your search
@@ -0,0 +1,20 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.data
|
||||||
|
dist
|
||||||
|
coverage
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.cursor
|
||||||
|
docs
|
||||||
|
tests
|
||||||
|
_prompts_tmp
|
||||||
|
_escopo_extraido.txt
|
||||||
|
*.tsbuildinfo
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.staging.example
|
||||||
|
config/staging-seed.json
|
||||||
|
Dockerfile
|
||||||
|
docker-compose*.yml
|
||||||
|
README.md
|
||||||
+61
-28
@@ -1,44 +1,77 @@
|
|||||||
# Identidade Elo Pro (Supabase Auth). Sem estes valores o módulo não autentica em produção.
|
# Desenvolvimento local. Para homologação use .env.staging.example.
|
||||||
|
# Confira a qualquer momento com: npm run verify:config
|
||||||
|
|
||||||
|
# --- Ambiente lógico -------------------------------------------------------
|
||||||
|
# development | staging | production. Independente de NODE_ENV.
|
||||||
|
ADAPTER_ENV=development
|
||||||
|
# Prefixo das chaves externas (Redis). Separa este aplicativo e este ambiente.
|
||||||
|
ADAPTER_NAMESPACE=adaptador-planilhas
|
||||||
|
|
||||||
|
# --- Sessão local sem senha (SOMENTE development) --------------------------
|
||||||
|
# Em staging e produção o provedor de desenvolvimento não existe no caminho de
|
||||||
|
# código, e ADAPTER_DEV_AUTH=1 faz o start ser recusado.
|
||||||
|
ADAPTER_DEV_AUTH=1
|
||||||
|
ADAPTER_DEV_AUTH_SECRET=troque-este-segredo-local
|
||||||
|
|
||||||
|
# --- Identidade Supabase (opcional em development) -------------------------
|
||||||
NEXT_PUBLIC_SUPABASE_URL=
|
NEXT_PUBLIC_SUPABASE_URL=
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||||
# Service role: somente servidor. Nunca prefixar com NEXT_PUBLIC_.
|
# Service role: somente servidor. Nunca prefixar com NEXT_PUBLIC_.
|
||||||
SUPABASE_SERVICE_ROLE_KEY=
|
SUPABASE_SERVICE_ROLE_KEY=
|
||||||
|
# Bucket privado. O valor literal "public" é recusado.
|
||||||
SUPABASE_STORAGE_BUCKET=
|
SUPABASE_STORAGE_BUCKET=
|
||||||
|
# Login próprio de homologação com Supabase Auth real. Só vale em staging.
|
||||||
|
ADAPTER_STAGING_AUTH=0
|
||||||
|
|
||||||
# PostgreSQL do Elo Pro. Obrigatório em produção.
|
# --- Banco ----------------------------------------------------------------
|
||||||
|
# Ausente em development usa PGlite em .data/pglite.
|
||||||
|
# Em staging e produção é obrigatória: não há fallback local.
|
||||||
DATABASE_URL=
|
DATABASE_URL=
|
||||||
|
# Decisão explícita, obrigatória fora de development.
|
||||||
|
# 1 = criar o shim local de identidade (profiles, workspaces, workspace_members)
|
||||||
|
# 0 = o schema do Elo Pro já existe; não recriar
|
||||||
|
ADAPTER_APPLY_IDENTITY_SHIM=1
|
||||||
|
|
||||||
# Redis. Obrigatório em produção para lock. Sem REDIS_URL o processamento recusa executar.
|
# --- Redis ----------------------------------------------------------------
|
||||||
|
# Ausente em development usa coordenação em memória.
|
||||||
|
# Em staging e produção é obrigatória: sem ela o processamento é recusado.
|
||||||
REDIS_URL=
|
REDIS_URL=
|
||||||
|
|
||||||
# Sentry. Sem DSN a instrumentação fica inerte.
|
# --- Processamento --------------------------------------------------------
|
||||||
SENTRY_DSN=
|
# inline processa no mesmo processo (somente development).
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
# worker enfileira no Redis e no Postgres. Obrigatório fora de development.
|
||||||
SENTRY_ENVIRONMENT=development
|
ADAPTER_JOB_MODE=inline
|
||||||
SENTRY_RELEASE=
|
# TTL de fontes e saídas, em segundos (1 a 604800). Obrigatório fora de
|
||||||
|
# development; ausente em development usa 3600.
|
||||||
|
SPREADSHEET_TEMP_TTL_SECONDS=3600
|
||||||
|
# Teto de um processamento, em milissegundos. Ausente usa 120000.
|
||||||
|
SPREADSHEET_JOB_TIMEOUT_MS=
|
||||||
|
ADAPTER_WORKER_TICK_MS=2000
|
||||||
|
ADAPTER_WORKER_BATCH=20
|
||||||
|
|
||||||
# Login central do ecossistema. O Adaptador não tem formulário de senha.
|
# --- Login central do ecossistema -----------------------------------------
|
||||||
|
# O Adaptador não tem formulário de senha própria.
|
||||||
ELO_PRO_LOGIN_URL=
|
ELO_PRO_LOGIN_URL=
|
||||||
# URL pública deste aplicativo. Usada para montar returnTo no mesmo host.
|
# URL pública deste aplicativo. Usada para montar returnTo no mesmo host.
|
||||||
ELO_PRO_APP_URL=
|
ELO_PRO_APP_URL=http://localhost:3000
|
||||||
# Hosts adicionais aceitos no returnTo, separados por vírgula. Caminho relativo sempre é preferido.
|
# Hosts adicionais aceitos no returnTo, separados por vírgula.
|
||||||
AUTH_ALLOWED_RETURN_HOSTS=
|
AUTH_ALLOWED_RETURN_HOSTS=
|
||||||
|
# Cookie de workspace ativo, definido pela plataforma: elo_pro_workspace_id.
|
||||||
|
# É preferência, não autorização: o servidor revalida a associação sempre.
|
||||||
|
|
||||||
# Cookie de workspace ativo definido pela plataforma Elo Pro.
|
# --- Observabilidade ------------------------------------------------------
|
||||||
# Nome lido pelo servidor: elo_pro_workspace_id
|
# Sem DSN a instrumentação fica inerte.
|
||||||
|
SENTRY_DSN=
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
|
# Ausente cai em ADAPTER_ENV.
|
||||||
|
SENTRY_ENVIRONMENT=
|
||||||
|
SENTRY_RELEASE=
|
||||||
|
# Upload de sourcemap só acontece com os três presentes no build.
|
||||||
|
SENTRY_ORG=
|
||||||
|
SENTRY_PROJECT=
|
||||||
|
SENTRY_AUTH_TOKEN=
|
||||||
|
|
||||||
# TTL de fontes e saídas, em segundos. Obrigatório em produção. Inteiro de 1 a 604800.
|
# --- Testes com serviços reais (npm run test:services) --------------------
|
||||||
# Em dev/teste, a ausência usa 3600. O número de produto continua configurável.
|
# Banco descartável: a suíte recria o schema public.
|
||||||
SPREADSHEET_TEMP_TTL_SECONDS=
|
ADAPTER_TEST_DATABASE_URL=
|
||||||
|
ADAPTER_TEST_REDIS_URL=
|
||||||
# Tempo máximo de um processamento, em milissegundos. Ausente usa 120000.
|
|
||||||
SPREADSHEET_JOB_TIMEOUT_MS=
|
|
||||||
|
|
||||||
# inline = processa no mesmo processo (somente dev/teste). worker = enfileira no Redis e no Postgres.
|
|
||||||
# Em produção, inline é recusado.
|
|
||||||
ADAPTER_JOB_MODE=worker
|
|
||||||
|
|
||||||
# Sessão local SEM senha, só fora de produção, para piloto sem o host Elo Pro.
|
|
||||||
# Produção recusa ADAPTER_DEV_AUTH=1.
|
|
||||||
ADAPTER_DEV_AUTH=0
|
|
||||||
ADAPTER_DEV_AUTH_SECRET=
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Homologação — hml-ada.elopro.app
|
||||||
|
# Copie para .env.staging no servidor e preencha. NÃO versione .env.staging.
|
||||||
|
# Confira com: npm run verify:config
|
||||||
|
|
||||||
|
# --- Ambiente lógico -------------------------------------------------------
|
||||||
|
# development | staging | production. Em homologação é sempre staging, mesmo
|
||||||
|
# com NODE_ENV=production (o build é o otimizado).
|
||||||
|
ADAPTER_ENV=staging
|
||||||
|
# Prefixo das chaves externas. Separa este aplicativo dos demais do ecossistema
|
||||||
|
# no Redis compartilhado e separa homologação de produção.
|
||||||
|
ADAPTER_NAMESPACE=adaptador-planilhas
|
||||||
|
|
||||||
|
# --- Identidade (Supabase Auth do projeto de HOMOLOGAÇÃO) ------------------
|
||||||
|
# Projeto Supabase próprio de homologação. Nunca o projeto de produção:
|
||||||
|
# credencial administrativa não é compartilhada entre aplicações nem ambientes.
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||||
|
# Somente servidor. Nunca prefixar com NEXT_PUBLIC_.
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=
|
||||||
|
# Bucket PRIVADO e exclusivo deste aplicativo neste ambiente.
|
||||||
|
# Confira com: npm run check:bucket
|
||||||
|
SUPABASE_STORAGE_BUCKET=adaptador-planilhas-hml
|
||||||
|
|
||||||
|
# Login próprio de homologação com Supabase Auth real (com senha, emitida e
|
||||||
|
# validada pelo Supabase). O dev auth sem senha NÃO funciona aqui.
|
||||||
|
# As contas são criadas pela operação no painel do Supabase de homologação.
|
||||||
|
ADAPTER_STAGING_AUTH=1
|
||||||
|
# Alternativa a ADAPTER_STAGING_AUTH: encaminhar ao SSO do Elo Pro quando existir.
|
||||||
|
ELO_PRO_LOGIN_URL=
|
||||||
|
ELO_PRO_APP_URL=https://hml-ada.elopro.app
|
||||||
|
AUTH_ALLOWED_RETURN_HOSTS=hml-ada.elopro.app
|
||||||
|
|
||||||
|
# --- Banco ----------------------------------------------------------------
|
||||||
|
# Banco de HOMOLOGAÇÃO. Nunca o de produção.
|
||||||
|
DATABASE_URL=
|
||||||
|
# Decisão explícita e obrigatória:
|
||||||
|
# 1 = criar o shim local de identidade (profiles, workspaces, workspace_members)
|
||||||
|
# 0 = o schema do Elo Pro já existe nesse banco; não recriar
|
||||||
|
# Rode `npm run migrate:check` antes de decidir.
|
||||||
|
ADAPTER_APPLY_IDENTITY_SHIM=1
|
||||||
|
|
||||||
|
# --- Redis (contêiner isolado deste aplicativo) ---------------------------
|
||||||
|
# Porta publicada só em 127.0.0.1 pelo docker-compose.staging.yml.
|
||||||
|
ADAPTER_REDIS_PASSWORD=
|
||||||
|
ADAPTER_REDIS_PORT=56380
|
||||||
|
REDIS_URL=redis://:SENHA_DO_REDIS@redis:6379
|
||||||
|
|
||||||
|
# --- Processamento --------------------------------------------------------
|
||||||
|
# worker é obrigatório fora de development. inline é recusado no start.
|
||||||
|
ADAPTER_JOB_MODE=worker
|
||||||
|
# Vida dos arquivos temporários, em segundos (1 a 604800). Obrigatório.
|
||||||
|
SPREADSHEET_TEMP_TTL_SECONDS=86400
|
||||||
|
# Teto de um processamento, em milissegundos. Ausente usa 120000.
|
||||||
|
SPREADSHEET_JOB_TIMEOUT_MS=120000
|
||||||
|
ADAPTER_WORKER_TICK_MS=2000
|
||||||
|
ADAPTER_WORKER_BATCH=20
|
||||||
|
# Carência do desligamento gracioso: espera o tick corrente liberar o lock.
|
||||||
|
# Precisa ser MENOR que o stop_grace_period do compose (30s).
|
||||||
|
ADAPTER_WORKER_GRACE_MS=20000
|
||||||
|
# Heartbeat lido pelo healthcheck do contêiner do worker.
|
||||||
|
ADAPTER_WORKER_HEARTBEAT=/tmp/adaptador-worker-heartbeat
|
||||||
|
|
||||||
|
# --- Observabilidade ------------------------------------------------------
|
||||||
|
# Projeto Sentry separado ou ambiente separado no mesmo projeto.
|
||||||
|
SENTRY_DSN=
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
|
SENTRY_ENVIRONMENT=staging
|
||||||
|
# Preenchido pelo pipeline com o SHA do commit.
|
||||||
|
SENTRY_RELEASE=
|
||||||
|
|
||||||
|
# --- Imagem e portas do compose -------------------------------------------
|
||||||
|
ADAPTER_IMAGE=ghcr.io/elopro/adaptador-planilhas:staging
|
||||||
|
ADAPTER_WEB_PORT=3110
|
||||||
|
|
||||||
|
# --- Smoke (fase 2, opcional; nunca comitar valores) ----------------------
|
||||||
|
# Conta de teste criada no painel do Supabase de HOMOLOGAÇÃO. Sem estas duas,
|
||||||
|
# a fase autenticada do smoke é reportada como PENDENTE, nunca aprovada.
|
||||||
|
ADAPTER_SMOKE_URL=https://hml-ada.elopro.app
|
||||||
|
ADAPTER_SMOKE_EMAIL=
|
||||||
|
ADAPTER_SMOKE_PASSWORD=
|
||||||
|
ADAPTER_SMOKE_WORKSPACE=
|
||||||
|
|
||||||
|
# --- Nunca definir em homologação -----------------------------------------
|
||||||
|
# ADAPTER_DEV_AUTH: o start é recusado com valor 1 fora de development.
|
||||||
|
# ADAPTER_DEV_AUTH_SECRET: não é usado fora de development.
|
||||||
@@ -5,6 +5,9 @@ dist
|
|||||||
coverage
|
coverage
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.staging
|
||||||
|
.env.production
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
_prompts_tmp
|
_prompts_tmp
|
||||||
_escopo_extraido.txt
|
_escopo_extraido.txt
|
||||||
|
config/staging-seed.json
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
# Imagem única para os dois processos do módulo: web (Next standalone) e worker.
|
||||||
|
# Rodar os dois do mesmo artefato garante que web e worker compartilhem
|
||||||
|
# exatamente o mesmo código e as mesmas dependências em homologação.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Base
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM node:24-bookworm-slim AS base
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dependências de produção. Inclui `tsx`, que o worker e os scripts
|
||||||
|
# operacionais usam em runtime — por isso `tsx` está em `dependencies`.
|
||||||
|
# `--ignore-scripts` é seguro aqui: nenhuma dependência de produção depende de
|
||||||
|
# postinstall, e o binário do esbuild vem do pacote de plataforma.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM base AS deps-prod
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dependências completas e build
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM base AS builder
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --ignore-scripts
|
||||||
|
COPY tsconfig.json next.config.ts postcss.config.mjs eslint.config.mjs next-env.d.ts ./
|
||||||
|
COPY src ./src
|
||||||
|
COPY migrations ./migrations
|
||||||
|
COPY scripts ./scripts
|
||||||
|
# Release do Sentry: o SHA do commit entra no bundle do cliente e no servidor,
|
||||||
|
# para o stack trace do evento apontar o código certo. Não é segredo.
|
||||||
|
ARG SENTRY_RELEASE=""
|
||||||
|
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||||
|
ENV NEXT_PUBLIC_SENTRY_RELEASE=$SENTRY_RELEASE
|
||||||
|
# O build não recebe segredo: `collectConfigIssues` é ignorado na fase de build
|
||||||
|
# e a validação acontece no start do container.
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Runtime
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM base AS runner
|
||||||
|
ARG SENTRY_RELEASE=""
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||||
|
ENV ADAPTER_WORKER_HEARTBEAT=/tmp/adaptador-worker-heartbeat
|
||||||
|
|
||||||
|
RUN groupadd --system --gid 1001 adaptador \
|
||||||
|
&& useradd --system --uid 1001 --gid adaptador adaptador
|
||||||
|
|
||||||
|
# 1) node_modules de produção primeiro. Traz `tsx` para o worker e os scripts.
|
||||||
|
COPY --from=deps-prod --chown=adaptador:adaptador /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# 2) Saída autocontida do Next por cima. Onde um pacote existe nas duas árvores,
|
||||||
|
# a versão traçada do build é a que vale para o servidor. Conferido: as duas
|
||||||
|
# árvores vêm do mesmo lockfile e não divergem em nenhuma versão; a árvore
|
||||||
|
# traçada tem 3 pacotes que não ficam no topo da de produção
|
||||||
|
# (@emnapi/runtime, isarray, supports-color), e o COPY mescla sem removê-los.
|
||||||
|
# O package.json do standalone NÃO é sobrescrito: é o que o server.js espera.
|
||||||
|
COPY --from=builder --chown=adaptador:adaptador /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=adaptador:adaptador /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
# 3) Arquivos lidos em runtime: migrations pelo migrate, src e scripts pelo
|
||||||
|
# worker, tsconfig pelos aliases `@/` do tsx.
|
||||||
|
COPY --chown=adaptador:adaptador migrations ./migrations
|
||||||
|
COPY --chown=adaptador:adaptador scripts ./scripts
|
||||||
|
COPY --chown=adaptador:adaptador src ./src
|
||||||
|
COPY --chown=adaptador:adaptador tsconfig.json ./
|
||||||
|
COPY --chown=adaptador:adaptador docker ./docker
|
||||||
|
|
||||||
|
USER adaptador
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Liveness não toca dependência externa: distingue processo vivo de dependência
|
||||||
|
# fora do ar, que é o papel de /api/health/ready. O contêiner do worker troca
|
||||||
|
# este healthcheck por `worker-health` no compose.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||||
|
CMD ["web"]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"_comentario": [
|
||||||
|
"Modelo do seed de homologacao. Copie para config/staging-seed.json e preencha.",
|
||||||
|
"users[].id e o UUID do usuario no Supabase Auth do projeto de homologacao.",
|
||||||
|
"Crie as contas no painel do Supabase: este arquivo nao guarda senha nem e-mail.",
|
||||||
|
"workspaces[].id pode ser qualquer UUID v4 estavel escolhido pela operacao.",
|
||||||
|
"Nao versione config/staging-seed.json."
|
||||||
|
],
|
||||||
|
"workspaces": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-4000-8000-000000000001",
|
||||||
|
"name": "Homologacao Alpha",
|
||||||
|
"moduleEnabled": false,
|
||||||
|
"allowNonAdminModelUpload": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-4000-8000-000000000002",
|
||||||
|
"name": "Homologacao Beta",
|
||||||
|
"moduleEnabled": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"users": [
|
||||||
|
{ "id": "00000000-0000-4000-8000-0000000000a1", "displayName": "Owner de homologacao" },
|
||||||
|
{ "id": "00000000-0000-4000-8000-0000000000a2", "displayName": "Agente de homologacao" }
|
||||||
|
],
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"workspaceId": "00000000-0000-4000-8000-000000000001",
|
||||||
|
"userId": "00000000-0000-4000-8000-0000000000a1",
|
||||||
|
"role": "owner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"workspaceId": "00000000-0000-4000-8000-000000000001",
|
||||||
|
"userId": "00000000-0000-4000-8000-0000000000a2",
|
||||||
|
"role": "agent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"workspaceId": "00000000-0000-4000-8000-000000000002",
|
||||||
|
"userId": "00000000-0000-4000-8000-0000000000a2",
|
||||||
|
"role": "agent"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Homologação em hml-ada.elopro.app, no servidor compartilhado do ecossistema.
|
||||||
|
#
|
||||||
|
# Regras que este arquivo respeita, por construção:
|
||||||
|
# - não declara, não reinicia e não depende de nenhum serviço de outro
|
||||||
|
# aplicativo: rede, volume e nome de projeto são exclusivos deste módulo;
|
||||||
|
# - não expõe porta em 0.0.0.0: só 127.0.0.1, para o Nginx fazer o proxy;
|
||||||
|
# - Redis é um contêiner isolado deste aplicativo, com senha e volume próprios,
|
||||||
|
# nunca o Redis compartilhado dos outros aplicativos;
|
||||||
|
# - Postgres não é declarado aqui: é o banco gerenciado do Elo Pro, informado
|
||||||
|
# em DATABASE_URL. Nenhuma migration roda no start — é passo manual aprovado.
|
||||||
|
#
|
||||||
|
# docker compose -f docker-compose.staging.yml --env-file .env.staging up -d --build
|
||||||
|
# docker compose -f docker-compose.staging.yml --env-file .env.staging \
|
||||||
|
# run --rm web migrate --dry-run
|
||||||
|
|
||||||
|
name: adaptador-planilhas-hml
|
||||||
|
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- "redis-server"
|
||||||
|
- "--requirepass"
|
||||||
|
- "${ADAPTER_REDIS_PASSWORD:?defina ADAPTER_REDIS_PASSWORD}"
|
||||||
|
- "--save"
|
||||||
|
- "900"
|
||||||
|
- "1"
|
||||||
|
- "--appendonly"
|
||||||
|
- "no"
|
||||||
|
- "--maxmemory"
|
||||||
|
- "256mb"
|
||||||
|
- "--maxmemory-policy"
|
||||||
|
- "noeviction"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${ADAPTER_REDIS_PORT:-56380}:6379"
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep -q PONG"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
environment:
|
||||||
|
REDIS_PASSWORD: ${ADAPTER_REDIS_PASSWORD}
|
||||||
|
networks: [adaptador-hml]
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: ${ADAPTER_IMAGE:-adaptador-planilhas:local}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: runner
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["web"]
|
||||||
|
env_file:
|
||||||
|
- .env.staging
|
||||||
|
environment:
|
||||||
|
ADAPTER_ENV: staging
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: "3000"
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${ADAPTER_WEB_PORT:-3110}:3000"
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- node
|
||||||
|
- -e
|
||||||
|
- "fetch('http://127.0.0.1:3000/api/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 20s
|
||||||
|
retries: 3
|
||||||
|
logging: &logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
networks: [adaptador-hml]
|
||||||
|
|
||||||
|
worker:
|
||||||
|
image: ${ADAPTER_IMAGE:-adaptador-planilhas:local}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: runner
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["worker"]
|
||||||
|
env_file:
|
||||||
|
- .env.staging
|
||||||
|
environment:
|
||||||
|
ADAPTER_ENV: staging
|
||||||
|
NODE_ENV: production
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
# SIGTERM chega ao processo do worker porque o entrypoint usa
|
||||||
|
# `node --import tsx` (sem processo filho). O handler espera o tick corrente
|
||||||
|
# para o `finally` liberar o lock. 30s cobrem um job médio.
|
||||||
|
stop_grace_period: 30s
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/app/docker/entrypoint.sh", "worker-health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 20s
|
||||||
|
retries: 3
|
||||||
|
logging: *logging
|
||||||
|
networks: [adaptador-hml]
|
||||||
|
|
||||||
|
networks:
|
||||||
|
adaptador-hml:
|
||||||
|
name: adaptador-planilhas-hml
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis-data:
|
||||||
|
name: adaptador-planilhas-hml-redis
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Reprodução local da topologia de homologação: mesma imagem, dois processos,
|
||||||
|
# Postgres e Redis de verdade. Serve para validar o artefato antes de subir ao
|
||||||
|
# servidor. Aqui o ambiente é `development`: não há Supabase local, então o
|
||||||
|
# armazenamento é um volume privado e a sessão usa o login local sem senha.
|
||||||
|
#
|
||||||
|
# docker compose up --build
|
||||||
|
# docker compose exec web /app/docker/entrypoint.sh migrate
|
||||||
|
#
|
||||||
|
# Nenhuma porta de serviço do ecossistema é usada: tudo em 127.0.0.1 e em
|
||||||
|
# portas altas, com nomes próprios de projeto, rede e volume.
|
||||||
|
|
||||||
|
name: adaptador-planilhas-local
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: adaptador
|
||||||
|
POSTGRES_PASSWORD: adaptador_local
|
||||||
|
POSTGRES_DB: adaptador
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:55432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U adaptador -d adaptador"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
networks: [adaptador]
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:56379:6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
networks: [adaptador]
|
||||||
|
|
||||||
|
web:
|
||||||
|
# Com ADAPTER_IMAGE definido, usa a imagem já construída (o CI faz isso).
|
||||||
|
# Sem ela, `up --build` constrói e marca com este nome.
|
||||||
|
image: ${ADAPTER_IMAGE:-adaptador-planilhas:local}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: runner
|
||||||
|
command: ["web"]
|
||||||
|
environment: &app-env
|
||||||
|
ADAPTER_ENV: development
|
||||||
|
ADAPTER_NAMESPACE: adaptador-planilhas
|
||||||
|
ADAPTER_JOB_MODE: worker
|
||||||
|
ADAPTER_APPLY_IDENTITY_SHIM: "1"
|
||||||
|
ADAPTER_DEV_AUTH: "1"
|
||||||
|
ADAPTER_DEV_AUTH_SECRET: segredo-apenas-local
|
||||||
|
DATABASE_URL: postgres://adaptador:adaptador_local@postgres:5432/adaptador
|
||||||
|
REDIS_URL: redis://redis:6379
|
||||||
|
SPREADSHEET_TEMP_TTL_SECONDS: "3600"
|
||||||
|
ELO_PRO_APP_URL: http://localhost:3000
|
||||||
|
PORT: "3000"
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:3000"
|
||||||
|
volumes:
|
||||||
|
- app-storage:/app/.data
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [adaptador]
|
||||||
|
|
||||||
|
worker:
|
||||||
|
image: ${ADAPTER_IMAGE:-adaptador-planilhas:local}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: runner
|
||||||
|
command: ["worker"]
|
||||||
|
environment: *app-env
|
||||||
|
volumes:
|
||||||
|
- app-storage:/app/.data
|
||||||
|
# O worker grava heartbeat ao fim de cada tick; o healthcheck detecta
|
||||||
|
# processo vivo mas travado, que um check de PID não pega.
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/app/docker/entrypoint.sh", "worker-health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 15s
|
||||||
|
retries: 3
|
||||||
|
stop_grace_period: 30s
|
||||||
|
depends_on:
|
||||||
|
web:
|
||||||
|
condition: service_started
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [adaptador]
|
||||||
|
|
||||||
|
networks:
|
||||||
|
adaptador:
|
||||||
|
name: adaptador-planilhas-local
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
name: adaptador-planilhas-local-postgres
|
||||||
|
app-storage:
|
||||||
|
name: adaptador-planilhas-local-storage
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Entrada única da imagem. Mantém os comandos curtos e garante que todos os
|
||||||
|
# scripts operacionais rodem no MESMO processo do contêiner.
|
||||||
|
#
|
||||||
|
# Por que `node --import tsx` e não o CLI do tsx: o CLI cria um processo filho,
|
||||||
|
# e o SIGTERM do `docker stop` pararia no pai. O worker precisa receber o sinal
|
||||||
|
# para esperar o tick corrente e liberar o lock no `finally`.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
TSX="--import=tsx"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
uso: entrypoint.sh <comando> [args]
|
||||||
|
|
||||||
|
web servidor Next (saída standalone)
|
||||||
|
worker processo de fila
|
||||||
|
migrate [--dry-run]
|
||||||
|
seed-staging
|
||||||
|
flag <workspace_id> on|off
|
||||||
|
cleanup
|
||||||
|
verify-config
|
||||||
|
check-bucket
|
||||||
|
smoke
|
||||||
|
worker-health confere o heartbeat do worker (healthcheck do contêiner)
|
||||||
|
exec <cmd...> qualquer outro comando
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="${1:-web}"
|
||||||
|
[ $# -gt 0 ] && shift || true
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
web) exec node server.js ;;
|
||||||
|
worker) exec node $TSX scripts/worker.ts ;;
|
||||||
|
migrate) exec node $TSX scripts/migrate.ts "$@" ;;
|
||||||
|
seed-staging) exec node $TSX scripts/seed-staging.ts "$@" ;;
|
||||||
|
flag) exec node $TSX scripts/set-flag.ts "$@" ;;
|
||||||
|
cleanup) exec node $TSX scripts/cleanup-temp.ts "$@" ;;
|
||||||
|
verify-config) exec node $TSX scripts/verify-config.ts ;;
|
||||||
|
check-bucket) exec node $TSX scripts/check-bucket.ts ;;
|
||||||
|
smoke) exec node $TSX scripts/smoke-staging.ts ;;
|
||||||
|
worker-health) exec node docker/worker-health.mjs ;;
|
||||||
|
exec) exec "$@" ;;
|
||||||
|
-h|--help|help) usage ;;
|
||||||
|
*) echo "comando desconhecido: $cmd" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Healthcheck do contêiner do worker. O worker grava o heartbeat ao fim de cada
|
||||||
|
// tick; se o arquivo envelhecer além da janela, o processo está vivo mas travado,
|
||||||
|
// que é o caso que um healthcheck de PID não detecta.
|
||||||
|
import { stat } from "node:fs/promises";
|
||||||
|
|
||||||
|
const file = process.env.ADAPTER_WORKER_HEARTBEAT ?? "/tmp/adaptador-worker-heartbeat";
|
||||||
|
const tick = Number(process.env.ADAPTER_WORKER_TICK_MS ?? 2000);
|
||||||
|
// Tolera dez ticks ou 60s, o que for maior: um job grande ocupa o tick inteiro.
|
||||||
|
const maxAgeMs = Math.max(tick * 10, 60_000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await stat(file);
|
||||||
|
const age = Date.now() - info.mtimeMs;
|
||||||
|
if (age > maxAgeMs) {
|
||||||
|
console.error(`heartbeat com ${Math.round(age / 1000)}s, limite ${Math.round(maxAgeMs / 1000)}s`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`heartbeat com ${Math.round(age / 1000)}s`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch {
|
||||||
|
console.error(`heartbeat ausente em ${file}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user