mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
16 KiB
16 KiB
Impl: Verify do CI (PR e deploy): um Postgres com reset determinístico, um build, migrate/seed sem repetição
Status: aprovado (gate humano 2026-08-24) Atualizado em: 2026-08-24 Issue: #834 Intenção: docs/plans/ops88-ci-verify-setup-unico.md Appetite restante: herdado (~0,5–1 dia eng; um outcome verificável)
Leitura da intenção
- Outcome: numa run de PR com build+e2e, o app compila uma única vez e integração, build e e2e rodam no mesmo Postgres, com reset determinístico entre as fases; o verify do deploy usa o mesmo setup único e continua gateando a publicação; migrates/seeds/builds repetidos caem (4 ciclos de reconstrução de estado → ~2), sem perda de garantia e sem re-run com ganho revertido.
- O que NÃO negociar: não virar "rodar menos testes" (PR continua
selected, deploy continuafull); não paralelizar o job único (fail-fast estrutural); o reset entre fases É a proteção do e2e contra resíduo do int — não removível; e2e sempre contra build de produção (.next-e2e), nunca dev; migrate-before-build embutido nopnpm build(contrato OPS66/deploy-homeserver) intacto; deploy homeserver intocado; guards de banco local/_test. - O que reavaliar: a "Direção no codebase" (workflows,
tests/helpers,playwright.config.ts) foi confirmada na exploração, com uma ressalva: o build default hoje não é servido por nada no CI (o e2e serve.next-e2ee o prod compila no homeserver) — isso valida a intenção B (colapsar só quando build+e2e rodam juntos) e o corte do build default no verify do deploy, onde e2e sempre roda. O "resíduo do int é finito e conhecido" também se confirmou: fixtures (campaignFixtures.ts), pins de contagem (435 municípios,municipalityCatalog.length + 1) e claims — nada que um drop-schema+migrate+seed não limpe.
Abordagem recomendada
flowchart LR
A["1 Postgres service (5432)"] --> B["migrate + seed minimal (como hoje)"]
B --> C["test:int (full/changed)"]
C --> D{"build ou e2e na run?<br/>build_mode != 'none' || e2e selected"}
D -- não --> F["fim (PR docs-only)"]
D -- sim --> E["db:reset<br/>(drop schema → migrate → seed minimal)"]
E --> G{"e2e selected?"}
G -- sim --> H["build .next-e2e (único build)"] --> I["e2e (specs selecionadas)"]
G -- não --> J["build default .next (único build)"]
Opções consideradas: A | B | C
Recomendação: A — um Postgres por run + script de reset dedicado entre as fases. O reset (drop schema → migrate → seed minimal) é exatamente o que o e2e precisa para não herdar resíduo do int; o segundo container era o workaround sem reset. O script é testável localmente, reusável no mirror gate-ci e protege o único ponto novo da arquitetura.
Rejeitadas:
- B — manter dois Postgres (status quo): não atende o aceite "um Postgres por run" e o ganho de tempo; o resíduo que o 2º container escondia continua sem solução determinística.
- C — reset inline no YAML (psql + pnpm): o runner hosted não tem
psql(o Postgres é service container, não ferramenta do runner); lógica de banco no YAML não é testável nem reusável no mirror local; duplicaria guards.
Decisões de engenharia (caras de reverter)
D1 — Onde vive o reset determinístico
- Opções:
- A) Script novo
scripts/db-reset.mjs+pnpm db:reset:DROP SCHEMA public CASCADE; CREATE SCHEMA public;via poolpg(já é devDependency — sem dependência depsqlno runner), depoisexecFileSyncdepnpm migrateepnpm db:seed:minimal(mesmo padrão dogate-ci.mjs). Guard:assertLocalDatabase(host local) e o contrato de nome^teqo(_[a-z0-9]+)?_test$(o mesmo detests/helpers/assertTestDatabase.ts:13) — um script que DROP SCHEMA não pode aceitarteqo(dev local tem dados de trabalho) nem nada remoto. - B) Passo YAML inline com
docker run postgres:17-alpine psql+pnpm migrate+pnpm db:seed:minimal. - C) Reusar
scripts/db-pull.mjs(ele já faz o drop emdb-pull.mjs:119).
- A) Script novo
- Recomendação: A — determinístico, testável localmente, reusável no
gate-ci.mjs(paridade CI↔local), espelha o shape doseed-minimal.mjs(guard +loadCliEnv). Nome: scriptpnpm db:resetnovo na famíliadb:*dopackage.json— não uma flag emdb:seed:minimal(o seed é idempotente e compartilhado por worktree/gate; não acoplar DROP a ele). Guard obrigatório por construção: host local (herdado deassertLocalDatabase) + sufixo_test— fail-closed antes do DDL. - Rejeitadas: B (sem
psqlno runner hosted; lógica no YAML = intocável fora do CI; guard duplicado e divergente); C (db-pullfaz pg_dump de prod com filtro de PII, restaura via container local — outro contrato; acoplar reset a ele o tornaria inexecutável no CI). - Detalhe do contrato compartilhado: exportar o regex de
_testdetests/helpers/assertTestDatabase.ts:13parascripts/lib/cli.mjs(fonte única —assertTestDatabase.tsjá importadefaultGatewayHostde lá), consumido por ambos. Alternativa aceitável (menor churn): duplicar o regex no script com comentário apontando o contrato — decidir no impl, favor à fonte única.
D2 — Ordem do job único e condições YAML (ci-pr.yml checks)
- Ordem nova: prep URL única (5432) → migrate + seed minimal (passos atuais) →
test:intfull/changed →pnpm db:reset(novo step) → build default OU build.next-e2e→ e2e. - Condições novas:
- Reset:
if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode == 'selected'— só quando há fase de build. - Build default:
if: build_mode != 'none' && e2e_mode != 'selected'(hoje sóbuild_mode) — só existe quando é o único build da run. No modofull(diff high-risk),e2e_mode == 'full'→ o build default continua rodando e o e2e segue pulado por design (OPS72); o classifier confirma:selectE2eSpecsdevolve{mode:'full', specs:[]}. - Build
.next-e2ee e2e: inalterados (e2e_mode == 'selected').
- Reset:
- Remover: service
postgres-build, prep/URL 5433 + migrate + seed. Manter o nome do servicepostgres-int(o allowlist de hosts segue válido; no runner hosted o alcance é vialocalhost:5432publicado — nome de bloco YAML não tem efeito funcional). Atualizar o comentário de cabeçalho que descreve os "two isolated services". - Matriz de casos (verificar no diff/review): docs-only (tudo
none) → migrate+seed rodam como hoje, reset/build/e2e skipam; PR com build sem e2e (modefull) → reset + build default; PR com e2eselected(com ou sem build surface) → reset + build.next-e2e(único) + e2e.
D3 — Migrate embutido no build
- Opções: A) aceitar o no-op —
pnpm buildre-rodapayload migratejá aplicado; B) remover/condicionar o migrate do build para o CI. - Recomendação: A — o migrate embutido é idempotente por design (Payload rastreia
payload_migrations) e opackage.jsonbuildé o mesmo binário que odeploy-homeserver.shusa para o build de PROD (OPS66: migrate-before-build obrigatório); mexer aí fere o contrato da publicação. Não tocar. - Rejeitada: B (variante com env/flag criaria dois caminhos de build; o contrato OPS66 é inegociável e o custo do no-op é de segundos).
- Contabilização honesta do aceite "4× → ~2×": ciclos completos de reconstrução de estado por run: 4 → 2 (baseline int + baseline build/e2e no MESMO Postgres). Postgres: 2 → 1. Builds: 2 → 1. Migrates executados (explícitos + embutidos): 4 → 3 — o residual é exatamente o no-op embutido deste ponto. Seeds: 2 → 2 (ver D5).
D4 — Verify do deploy
- Opções: A) mesmo setup único no job
verify(deploy.yml), mantendo o fluxo full; B) reduzir o verify ao setup novo. - Recomendação: A — rede de segurança da publicação inegociável.
verifyganha: 1 service (removerpostgres-build), prep URL única, migrate+seed, int full,pnpm db:reset, e apenas o build.next-e2e+ e2e full — o build default sai porque no verify o e2e sempre roda, então o único build necessário é o que o Playwright serve. Jobdeploy(self-hosted homeserver) intocado. - Rejeitada: B (verify reduzido = publicar com garantia menor; contradiz o aceite).
D5 — Seeds
- Opções: A) seed dentro do reset (drop → migrate → seed minimal); B) reset só drop+migrate (confiar que o seed baseline sobrevive ao int).
- Recomendação: A — o seed minimal é idempotente e o e2e depende do baseline seedado (paridade worktree/CI, OPS28 + guard
seedParity; os pins depriority/expectedVotes/campaignGoalse os users sintéticos). Re-rodar o seed pós-int é a prova barata de baseline idêntico (a intenção exige: "quem executa deve provar o reset antes de confiar no banco único"). Custo ~10–30s; não otimizar fora disso. - Rejeitada: B (depende de provar que nenhuma spec int toca rows do seed — consent rename, pins, goals — sem ganho que compense; a garantia "e2e e int compartilham o MESMO baseline" só vale com o seed no reset).
Componentes / mudanças
scripts/db-reset.mjs(novo): DDLDROP SCHEMA public CASCADE; CREATE SCHEMA public;via poolpg(devDependency já presente) →execFileSync('pnpm', ['migrate'])→execFileSync('pnpm', ['db:seed:minimal']); guard duplo fail-closed (host local viaassertLocalDatabase+ regex_test); log de fases; exit não-zero fail-fast. ReusaloadCliEnvdescripts/lib/cli.mjs.scripts/lib/cli.mjs: exporta o contratoTEST_DATABASE_NAME_RE(fonte única comassertTestDatabase.ts).package.json:"db:reset": "node scripts/db-reset.mjs"(famíliadb:*).package.jsonjá está emHIGH_RISK_EXACT→ o PR desta entrega classificafulle roda int cheia + build..github/workflows/ci-pr.yml: removepostgres-build+ steps 5433; adiciona step "Reset database (single service)" com oif:de D2; ajustaif:do build default; atualiza comentário de serviços..github/workflows/deploy.yml(jobverify): espelho do acima, sem skips — removepostgres-build, remove build default, adiciona reset entre int e build.next-e2e.scripts/gate-ci.mjs:pnpm db:resetantes do build quando há fase de build — paridade local com o CI novo (o mirror deixa de construir sobre resíduo do int).scripts/lib/test-affected-core.mjs:HIGH_RISK_EXACT+='scripts/db-reset.mjs'e'scripts/lib/cli.mjs'(mudança no reset ou no contrato_testé harness, deve rodar suíte cheia);BUILD_SURFACE_EXACT+='scripts/db-reset.mjs'— sem isso um PR só no reset classificariabuild_mode='none'e o próprio step Reset pularia no CI (achado do simplify).- Docs: entrada em
docs/changelog/<data>-ops88.md+pnpm changelog:build; menção curta no AGENTS.md (bloco de CI) se o runbook do job mudar — o runbook de deploy (docs/ops/teqo-1313-deploy.md) não muda. - Migration: sem migration (nenhum schema/dado novo).
- Access / Consent: nenhum (nenhuma chave de Consent tocada; fail-closed intacto).
- UI: N/A — sem UI (Impeccable A).
Fases verificáveis
- Tracer — script de reset (quota principal):
scripts/db-reset.mjs+pnpm db:reset+ contrato compartilhado. Verificar localmente num banco de teste: (a) banco com resíduo (rodarpnpm test:intfull antes) → reset → schema zerado, migrate+seed reaplicados, 435 municípios e pins presentes; (b) idempotência (re-execução OK); (c) guards: recusateqo(sem_test) e host remoto; (d)pnpm test:intfull verde pós-reset. - Workflows e mirror: editar
ci-pr.yml,deploy.yml,gate-ci.mjs; verificar a matriz de casos de D2 no diff;pnpm gate:cilocal (inclui o reset novo) verde. O caminho e2e novo do PR só é exercitado por um PRselected— o PR desta entrega classificafull(package.json), então a prova do caminho e2e é: localpnpm db:reset && NEXT_DIST_DIR=.next-e2e pnpm build+pnpm test:e2e:affected(skill, OPS72) e, pós-merge, umdeploymanual (verify full) como prova canônica. - Gates:
pnpm gate:fastna iteração;pnpm check:cycles/format:check/knip; changelog entry; push viapnpm push.
Rabbit holes / Não escopo (engenharia)
- Não tocar em
playwright.config.ts,next.config.mjs, workers/retries, suítes/specs, classifier de e2e (OPS86) nem níveis de teste (OPS87). - Não alterar o script
builddopackage.jsonnemdeploy-homeserver.sh(contrato OPS66) — D3. - Não renomear o service para
postgres(churn no allowlist de hosts sem ganho funcional). - Não dar
if:aos passos migrate+seed iniciais para pular docs-only PRs (comportamento atual preservado; fora do outcome). - Não adicionar artefato/shared de
.nextentre fases (job único não precisa; fora de escopo). - Não mexer no lease de banco (
testDatabaseLease.tsé serialização intra-run via advisory lock — não é reset). - Não rodar
pnpm db:resetcontrateqo(dev local sem_test) — o guard é obrigatório, não um aviso. - Não engenhar fallback se o reset falhar no meio (drop feito, migrate falhou): a run falha e a re-run parte da VM nova com service fresco — estado limpo por construção.
Riscos e mitigação
- Resíduo não coberto pelo drop schema (sequences/views/grants):
DROP SCHEMA public CASCADE; CREATE SCHEMA public;cobre tabelas, views e sequences do schema; extensions permanecem (as mesmas de sempre); migrates reaplicam o schema completo. Verificação no tracer (1a) roda int full antes do reset — o caso real. - PR desta entrega não exercita o caminho e2e do CI (classifica
full): mitigado por prova local (db:reset+ build.next-e2e+test:e2e:affected) e pelo deploy verify manual pós-merge. - Custo do reset (~10–30s por run): mais que compensado pela remoção de um Postgres boot + um ciclo migrate+seed completo (~30–60s+); líquido negativo. Medir o job na 1ª run pós-merge.
- Condição YAML errada (matriz de casos): as condições são poucas e exclusivas (reset/build-default/e2e-build); revisar a matriz de D2 no PR body;
ciSkipInvariants.unit.spec.tscobre o classifier, não o YAML — o checklist manual fecha o gap. - Reset com conexões ativas: o reset roda entre fases, quando vitest/playwright já saíram; o pool
pgfecha após o DDL; sem concorrência possível na ordem serial do job. psqlausente no runner hosted: resolvido por construção (DDL viapgem node).
Aceite de engenharia
- Aceite de produto da intenção ainda coberto: 1 Postgres por run com reset determinístico entre fases; 1 build quando build+e2e rodam juntos (default só quando é o único); verify do deploy full com setup compartilhado; deploy homeserver intacto; mensurável — ciclos 4× → ~2×, builds 2 → 1
- Invariantes AGENTS/engineering-standards: guard de banco local/
_testfail-closed no novo script; nenhum schema/access/Consent tocado; contratos públicos, OPS66 e guards existentes preservados - Testes de domínio previstos: unit do contrato do guard (se a const compartilhada nascer em
cli.mjs), int full local pós-reset (pins 435/catalog+1), e2e local pós-reset (affected),gate-cilocal com o reset
Self-score decision-quality: 5/5
- Decisões caras têm rejeitadas? Sim — as 5 decisões (D1–D5) têm Opções/Recomendação/Rejeitadas explícitas.
- Cabe no appetite? Sim — um script novo + edits em 2 workflows/mirror/manifest, com prova local, dentro de ~0,5–1 dia.
- Rabbit holes nomeados? Sim — 9 cortes explícitos de engenharia, cada um com o dono do contrato.
- Depth check: reusa helpers existentes? Sim —
assertLocalDatabase,loadCliEnv,pg, famíliadb:*, padrãoexecFileSyncdogate-ci.mjs,HIGH_RISK_EXACTe o regex deassertTestDatabasecompartilhado. - Aceite de produto preservado? Sim — nenhuma redução de suíte, e2e sempre em build de produção, OPS66 e deploy homeserver intactos, com contabilização honesta do "4× → ~2×".