O RUN do builder encadeia gerador de importMap e next build com '&&', mas prefixos de shell (VAR=x cmd1 && cmd2) valem só para o primeiro comando. Com o OPS99, NEXT_OUTPUT_STANDALONE=1 e NODE_OPTIONS ficaram presos no gerador: o next build rodou sem output standalone e o stage runner falhou no COPY de /app/.next/standalone (run 34701266297, pós-#953 — o #953 já corrigiu o importmap sem react-server e o falso verde do stale run). Cada comando passa a declarar seu próprio env, e o guard de invariantes do Dockerfile em tests/unit/deployScript.unit.spec.ts ganha o pin do escopo (flags no lado do next build). Inclui post-mortem e changelog.
14 KiB
Post-mortem: build da imagem mudo desde o OPS99 — importmap sem react-server e envs presos no prefixo
Template do
/bug-fix. Preencha com fatos apurados; o que não for apurado fica "não apurado" — nunca invente.
Registro
| Campo | Valor |
|---|---|
| Data do post-mortem | 2026-09-12 |
| Severidade | alta (nenhum build de imagem completou desde o OPS99; nenhum dado/PII afetado; prod no build anterior) |
| Ambiente | prod (build da imagem no homeserver, via deploy.yml) |
| Issue(s) | sem Issue (fluxo /bug-fix) |
| PR do fix | #953 (importmap + stale run) e #952 (envs do standalone + guards + docs) |
| Detectado por | humano (resultado do deploy manual) + log do run 34696817841 |
Timeline
| Momento | Data/hora | Evento |
|---|---|---|
| Início provável | 2026-08-27 16:05 -0300 | commit 493f4ba9 (OPS99) encadeia o gerador no RUN do builder sem --conditions=react-server e deixa os prefixos de env presos no primeiro comando da cadeia |
| Detecção | 2026-09-12 13:34–14:00 UTC | run 34696817841 do deploy.yml: verify verde (16m56s), deploy homeserver (teqo-1313) falha em 9m34s no docker build — Error: This module cannot be imported from a Client Component module no passo do importmap |
| Correção parcial mergeada | 2026-09-12 14:50 UTC | PR #953 (commit 52a2ef6d): o gerador passa a usar pnpm generate:importmap e o guard de stale run deixa de sair 0 em falso verde |
| Segunda falha confirmada | 2026-09-12 15:07–15:33 UTC | run 34701266297 (dispatch pós-#953): importmap passa, next build completa (399s) sem .next/standalone e o stage runner morre em COPY --from=builder /app/.next/standalone ./ → "/app/.next/standalone": not found (Dockerfile:73) |
| Correção completa | pendente | PR #952 — envs do standalone no comando do next build, guards de invariantes e este documento; CI em andamento |
| Deploy | pendente | dispatch manual do deploy.yml pelo humano após o merge (o deploy é manual; nenhum merge publica sozinho) |
| Verificado em prod | pendente | confirmação do humano após o deploy |
Antes do OPS99, o último build real de imagem que passou foi o run 33074305879 (2026-08-27 12:56 UTC, prod no commit 4d6533c04 conforme o commit do #953). O "sucesso" das 05:22 (run 34675436246) foi falso verde do guard de stale run: main avançou durante o verify (087635d0 vs ccc8d6ea) e o script saiu 0 sem deployar — comportamento corrigido no #953. Desde o OPS99 nenhum build de imagem completou, e o primeiro rebuild real (dispatch das 13:34) expôs os dois defeitos do mesmo RUN.
O bug
O deploy manual de 2026-09-12 13:34 UTC não chegou ao fim: o job deploy homeserver (teqo-1313) falhou no docker build (target builder), no passo pnpm exec payload generate:importmap, com Error: This module cannot be imported from a Client Component module (node_modules/server-only/index.js). Produção continuou no build anterior; nenhum dado, PII ou migration foi tocado. Sintoma — não a causa.
A correção do #953 destravou o primeiro obstáculo e desarmou o falso verde do stale run, mas deixou o segundo defeito latente no mesmo RUN: NEXT_OUTPUT_STANDALONE=1 e NODE_OPTIONS continuavam prefixando o primeiro comando da cadeia (o gerador), então o next build sairia sem output: 'standalone' e o stage runner falharia no COPY /app/.next/standalone — o deploy seguiria sem publicar. O run 34701266297 (dispatch pós-#953) confirmou a segunda falha em produção: o importmap passou, o next build completou (399s) sem gerar .next/standalone e o build morreu no COPY --from=builder /app/.next/standalone ./ → "/app/.next/standalone": not found (Dockerfile:73).
Causa-raiz
Os dois defeitos têm a mesma raiz: o RUN do builder executa o gerador e o next build na mesma cadeia &&, e o OPS99 (commit 493f4ba9) montou essa cadeia reimplementando a invocação do CLI no Dockerfile em vez de chamar o script dono (package.json → generate:importmap), sem atentar para a semântica de prefixos do shell. Cada defeito é uma face disso:
- Importmap sem
react-server(corrigido no #953): o script do repo defineNODE_OPTIONS="--no-deprecation --conditions=react-server"(package.json; mesmo contrato emscripts/predev-importmap.mjs:41), mas o Dockerfile chamoupnpm exec payload generate:importmapdireto. Sem a condição,server-onlyresolve para o módulo que lança; com ela, para oempty.js. O CLI carrega opayload.config.tsinteiro — que importasrc/utilities/campaignAccess.ts→src/utilities/access/campaignStaffAdvisors.ts:5(import 'server-only') — e o build morria na avaliação do config. - Envs presos no prefixo (corrigido neste PR): em shell POSIX, atribuições-prefixo (
VAR=x cmd1 && cmd2) valem só para o primeiro comando (reproduzido:sh -c 'FOO=1 sh -c "echo first=\$FOO" && sh -c "echo second=\$FOO"'→first=1,second=). Ao prepender o gerador, o OPS99 deixouNEXT_OUTPUT_STANDALONE=1(lido emnext.config.mjs:8) eNODE_OPTIONS="--max-old-space-size=8000"aplicados ao gerador; onext buildrodaria sem standalone.
5-whys:
- Por que o deploy falhou? O
generate:importmapdo Docker build estourou numimport 'server-only'do grafo dopayload.config.ts. - Por que estourou? O Payload CLI rodou sem
--conditions=react-server, a condição que fazserver-onlyvirar módulo vazio. - Por que rodou sem a condição? O Dockerfile re-escreveu a chamada do CLI em vez de usar o script
pnpm generate:importmap, que é o dono da flag — divergiu do contrato sem ninguém notar. - Por que o PR CI não pegou? O CI de PR não builda a imagem Docker; o único lugar que exerce o stage
builderé o deploy manual, e os dispatches pós-OPS99 não tinham feito rebuild real (o das 05:22 foi stale skip). - Por que a divergência ficou 2 semanas escondida? O contrato do RUN (comando canônico + escopo dos envs) vivia só na cabeça de quem escreveu o OPS99: nada pinava o Dockerfile aos scripts donos nem cobria o encadeamento do build da imagem em teste.
Evidência: falha de produção do importmap reproduzida localmente em 2026-09-12 com o comando exato do Dockerfile (mesma stack, server-only/index.js:1); a mesma invocação via script (pnpm generate:importmap) passa. A semântica de prefixo foi reproduzida com o one-liner acima; o build de produção local sem NEXT_OUTPUT_STANDALONE=1 deixa de gerar .next/standalone, e o run 34701266297 confirmou a falha no COPY do stage runner.
Correção
- #953 (commit
52a2ef6d, já em main): o Dockerfile passa a chamarpnpm generate:importmap(script dono da flagreact-server, o mesmo dopredev); o guard de stale run passa desay … exit 0parafatalnos dois pontos, sem falso verde. - #952 (este PR): cada lado da cadeia declara seu próprio env —
pnpm generate:importmap && NEXT_OUTPUT_STANDALONE=1 NODE_OPTIONS="--no-deprecation --max-old-space-size=8000" pnpm exec next build. Onext buildvolta a produzir.next/standalonepara o stagerunnere mantém o teto de memória. - Guards (
tests/unit/deployScript.unit.spec.ts): (a) o scriptgenerate:importmapdopackage.jsoncontém--conditions=react-servere o Dockerfile usa o script, não o CLI inline (#953, estendido com o pin do script); (b)NEXT_OUTPUT_STANDALONE=1/max-old-space-size=8000ficam no lado donext builde não no lado do gerador (#952); (c) os dois pontos de stale run sãofatal(#953).
Resolve a causa: o build da imagem volta a usar a invocação canônica do CLI e não depende mais de onde o && corta os envs. Sem migration, sem mudança de access ou Consent.
Verificação
- Teste de regressão do importmap (#953):
Dockerfile: generates the importMap via the canonical script— falha com o Dockerfile do OPS99 restaurado (mutação, 1 failed / 11 passed) e passa com o fix - Teste de regressão dos envs (#952):
Dockerfile: NEXT_OUTPUT_STANDALONE and NODE_OPTIONS reach next build (shell prefix scoping)— falha com o Dockerfile do #953 (main atual, 1 failed / 11 passed) e passa com o fix (12 passed) - Reprodução: comando exato do OPS99 falha localmente (mesma stack
server-only);pnpm generate:importmappassa; one-liner de shell prova a semântica de prefixo - Produção-equivalente local:
pnpm generate:importmap(S3 dummy) gera o importMap com oS3ClientUploadHandler(entrada que o OPS99 protegia) eNEXT_OUTPUT_STANDALONE=1 NODE_OPTIONS="--no-deprecation --max-old-space-size=8000" pnpm exec next buildcompleta e produz.next/standalone/server.js - Suíte:
pnpm gate:fastverde (lint + typecheck + unit);pnpm pushverde no gate:ci local (int 78 files / 712 tests, docs guards) - CI: pendente — PR #952
- Prod: run 34701266297 (pós-#953) confirmou a segunda falha —
"/app/.next/standalone": not foundno stagerunner(Dockerfile:73); deploy verificado pendente — dispatch manual dodeploy.ymlpelo humano após o merge deste PR, confirmação do humano pendente
Prevenção
| Estratégia | Custo | Estado |
|---|---|---|
Gerador via script canônico (pnpm generate:importmap) e stale run fatal, pinados por unit |
barata | implementada no #953 |
Guard unit pinando o escopo dos envs no lado do next build da cadeia |
barata | implementada agora (PR #952) |
Job de CI que builda o stage builder da imagem (Docker + DB migrado + secrets) a cada mudança de Dockerfile/script |
cara | documentada — não implementada |
| Lint que proíbe invocação inline de CLI Payload em Dockerfile (fora dos scripts donos) | cara | documentada — candidata a Issue futura |
Destrackear src/app/(payload)/admin/importMap.js (segue tracked no git apesar do .gitignore da OPS99) |
barata | documentada — fora deste fluxo |
Estratégia implementada: guards unitários dos invariantes do caminho de build Docker (comando canônico do importmap, escopo dos envs no next build, stale run fatal) — a classe "o Dockerfile divergiu do contrato dos scripts" vira falha de teste no PR, não falha de deploy em produção.
Estratégia documentada (cara): job de CI que executa o build da imagem (ou ao menos docker build --target builder) a cada mudança de Dockerfile/scripts, e lint que bane invocação inline do CLI Payload no Dockerfile. Exigem runner com Docker + banco migrado + secrets de build — candidatas a Issue futura. O destracking do importMap.js também fica registrado como limpeza fora do escopo.
Lições
- Uma causa, duas falhas latentes: o mesmo
&&do OPS99 quebrou o importmap e roubou os envs donext build. Corrigir a primeira (importmap) não zerou o incidente — o run seguinte mostrou que metade do problema continuava armada. Um fix completo remove a causa (o encadeamento mal montado), não só o sintoma visível. - Cadeia com
&&muda o dono dos prefixos de env: no shell,VAR=x cmd1 && cmd2dáVARsó aocmd1. Flags de build devem ficar explícitas no comando que as consome. - "Deploy verde" pode não ter deployado: o stale run das 05:22 saiu 0 em 19s e o GitHub mostrou sucesso — prod ficou duas semanas num build antigo. Falso verde de pipeline é pior que falha explícita; o #953 corrigiu o stale run e o run pós-#953 comprovou a segunda falha em vez de escondê-la.
- Reimplementar a invocação em vez de chamar o script dono é uma twin barata de nascer e cara de pagar: o
predev-importmap.mjsjá usava a condiçãoreact-server; o Dockerfile duplicou o comando e perdeu a flag. Chamar o script teria custado zero. - O único lugar que exercita o Dockerfile não era o PR CI: guards de invariantes do arquivo de build são o mínimo para compensar a ausência de um job de imagem no CI.