From 99b36374c86bd81ce3829cc4c64c4cc2ee5c83de Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 24 Aug 2026 14:33:16 -0300 Subject: [PATCH] =?UTF-8?q?review(OPS88):=20simplify=20=E2=80=94=20build?= =?UTF-8?q?=20surface=20do=20reset,=20contrato=20high-risk,=20erros=20rotu?= =?UTF-8?q?lados,=20docs=20can=C3=B4nicos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-pr.yml | 6 ++-- .github/workflows/deploy.yml | 8 ++--- docs/AGENT-OPS.md | 16 ++++----- docs/CHANGELOG-AGENTS-HISTORY.md | 2 ++ docs/TESTING.md | 2 +- docs/changelog/2026-08-24-ops88.md | 2 +- .../plans/ops88-ci-verify-setup-unico-impl.md | 4 +-- scripts/db-reset.mjs | 34 ++++++++++++------- scripts/gate-ci.mjs | 17 ++++++---- scripts/lib/cli.mjs | 10 +++--- scripts/lib/test-affected-core.mjs | 4 +++ tests/helpers/assertTestDatabase.ts | 9 ++--- tests/unit/localHosts.unit.spec.ts | 2 +- 13 files changed, 69 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 8c906bc7..f2bdb787 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -155,7 +155,7 @@ jobs: - name: Unit tests (changed since PR base, full on empty selection) if: steps.scope.outputs.test_mode == 'changed' run: node scripts/vitest-changed-or-full.mjs --suite unit --base origin/${{ github.base_ref }} - - name: Prepare database URL (int service) + - name: Prepare database URL run: | set -euo pipefail echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" >> "$GITHUB_ENV" @@ -163,9 +163,9 @@ jobs: # must win through the .env.test.local layer (same mechanism # `pnpm worktree next` uses). echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" > .env.test.local - - name: Migrate (int database) + - name: Migrate run: pnpm migrate - - name: Seed minimal (int database) + - name: Seed minimal run: pnpm db:seed:minimal - name: Integration tests (full suite) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ec36e8ef..6450ddc5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -86,7 +86,7 @@ jobs: - name: Unit tests (full suite) run: pnpm test:unit - - name: Prepare database URL (int service) + - name: Prepare database URL run: | set -euo pipefail echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" >> "$GITHUB_ENV" @@ -94,9 +94,9 @@ jobs: # must win through the .env.test.local layer (same mechanism # `pnpm worktree next` uses). echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" > .env.test.local - - name: Migrate (int database) + - name: Migrate run: pnpm migrate - - name: Seed minimal (int database) + - name: Seed minimal run: pnpm db:seed:minimal - name: Integration tests (full suite) env: @@ -106,7 +106,7 @@ jobs: VITEST_MAX_WORKERS: 4 run: pnpm test:int - - name: Reset database (single service) + - name: Reset database (drop → migrate → seed) # OPS88: one Postgres per run — the e2e build starts from the same # clean baseline (drop schema → migrate → seed:minimal) instead of a # second container. The full suite always runs here, so only the diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index db933a6b..0b5ea511 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -74,14 +74,14 @@ Arquivos de "lista" que N agentes escrevem em paralelo (plans, migrações, cata ## CI por alvo (GitHub Actions, OPS71) -| Workflow | Trigger | Banco | Passos | -| ------------------------------------ | --------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ci-pr.yml` | PR → `main` (same-repo) | 2× service Postgres 17 | Job único `checks` sequencial (OPS62 X1): guards → lint→format→typecheck→knip→cycles→unit→int→build→e2e; skips via `ci-scope.mjs` (step `scope`); **fail-fast estrutural** — step vermelho interrompe o job, o run falha; `concurrency` nativo cancela o run em push novo; e2e **apenas blast radius** — `selected` ou, em high-risk, o conjunto `curated` (nunca `full` — OPS72/OPS86); área de risco sem mapping falha `unmapped-risk`; unit/int `changed` com seleção vazia cai em full (`vitest-changed-or-full.mjs`) — em 1 processo com `PLAYWRIGHT_WORKERS=4`; 2 services por nome na rede do job (`postgres-int`/`postgres-build`, sem publish host); fork-PR excluída no `if:` do job (repo público — não queima minutes) | -| `deploy.yml` | **`workflow_dispatch` manual** | 2× service Postgres 17 | **Dois jobs:** `verify` (hosted, suíte **full** sem skips — lint→format→typecheck→knip→cycles→unit→int→build→e2e full) → `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`) executa `scripts/deploy-homeserver.sh ` **localmente** no homeserver (sem SSH; idempotente; HEAD guard; flock; migrator→migrate→runner→rollout→smoke) | -| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Closes`/`Fixes` → `done` + `in-prod` + **close** **no GitHub** (token `GITHUB_TOKEN`; body do PR via `PR_BODY` do evento GitHub — no dispatch, busca via `gh` com o mesmo contrato: merged + main + same-repo) | -| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Related #N` aguardando plano → `ready` **no GitHub** (OPS18; soft-skip; mesmo token) | -| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `github-pr-automerge.mjs` (plain Node, REST+GraphQL): draft `cursor/*` → Ready (PATCH); arm `enablePullRequestAutoMerge` (rebase) **com o `AUTOMERGE_PAT`** (fail-closed — nunca o `GITHUB_TOKEN` nativo, cujo merge como `github-actions[bot]` não cria runs no `closed`; OPS71-FLIP) — o **servidor** espera o required check `CI (PR) / checks` (o OPS64 "rollup pode mentir" é estrutural aqui: job único posta um check-run honesto); draft não-`cursor/*` = veto, skip | -| `archive-cursor-agent.yml` | dispatch manual (ops) | — | Helper: cancela run + arquiva um agente Cursor Cloud (`CURSOR_API_KEY`) — **dormente (OPS65)** como o pool | +| Workflow | Trigger | Banco | Passos | +| ------------------------------------ | --------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ci-pr.yml` | PR → `main` (same-repo) | 1× service Postgres 17 | Job único `checks` sequencial (OPS62 X1): guards → lint→format→typecheck→knip→cycles→unit→int→build→e2e; skips via `ci-scope.mjs` (step `scope`); **fail-fast estrutural** — step vermelho interrompe o job, o run falha; `concurrency` nativo cancela o run em push novo; e2e **apenas blast radius** — `selected` ou, em high-risk, o conjunto `curated` (nunca `full` — OPS72/OPS86); área de risco sem mapping falha `unmapped-risk`; unit/int `changed` com seleção vazia cai em full (`vitest-changed-or-full.mjs`) — em 1 processo com `PLAYWRIGHT_WORKERS=4`; 1 service `postgres-int` (localhost:5432) com **`pnpm db:reset`** (drop→migrate→seed:minimal) entre int e build/e2e (OPS88 — substitui o 2º container); fork-PR excluída no `if:` do job (repo público — não queima minutes) | +| `deploy.yml` | **`workflow_dispatch` manual** | 1× service Postgres 17 | **Dois jobs:** `verify` (hosted, suíte **full** sem skips — lint→format→typecheck→knip→cycles→unit→int→`db:reset`→build `.next-e2e`→e2e full; OPS88: um build só, o default saiu) → `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`) executa `scripts/deploy-homeserver.sh ` **localmente** no homeserver (sem SSH; idempotente; HEAD guard; flock; migrator→migrate→runner→rollout→smoke) | +| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Closes`/`Fixes` → `done` + `in-prod` + **close** **no GitHub** (token `GITHUB_TOKEN`; body do PR via `PR_BODY` do evento GitHub — no dispatch, busca via `gh` com o mesmo contrato: merged + main + same-repo) | +| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Related #N` aguardando plano → `ready` **no GitHub** (OPS18; soft-skip; mesmo token) | +| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `github-pr-automerge.mjs` (plain Node, REST+GraphQL): draft `cursor/*` → Ready (PATCH); arm `enablePullRequestAutoMerge` (rebase) **com o `AUTOMERGE_PAT`** (fail-closed — nunca o `GITHUB_TOKEN` nativo, cujo merge como `github-actions[bot]` não cria runs no `closed`; OPS71-FLIP) — o **servidor** espera o required check `CI (PR) / checks` (o OPS64 "rollup pode mentir" é estrutural aqui: job único posta um check-run honesto); draft não-`cursor/*` = veto, skip | +| `archive-cursor-agent.yml` | dispatch manual (ops) | — | Helper: cancela run + arquiva um agente Cursor Cloud (`CURSOR_API_KEY`) — **dormente (OPS65)** como o pool | `agent-pool.yml` foi **removido no OPS65** (pool dormente; o tick `*/10` custava 144 `pnpm install`/dia na workstation). O verificador de main (`ci.yml`) foi **eliminado no OPS71** (gate 2026-08-19): só o `verify` do dispatch manual roda a suíte full. diff --git a/docs/CHANGELOG-AGENTS-HISTORY.md b/docs/CHANGELOG-AGENTS-HISTORY.md index 0ec2e55a..41aa1183 100644 --- a/docs/CHANGELOG-AGENTS-HISTORY.md +++ b/docs/CHANGELOG-AGENTS-HISTORY.md @@ -5,6 +5,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**OPS88 — verify do CI (PR e deploy): um Postgres com reset determinístico, um build, migrate/seed sem repetição (2026-08-24):** o `checks` do PR e o `verify` do deploy reconstruíam o mesmo estado até 4× por run (dois containers Postgres `postgres-int`/`postgres-build`, dois migrates explícitos, dois seeds, dois builds — cada build re-rodando migrate embutido; o build default `.next` nem era servido por nada). Agora: **um Postgres por run** com reset determinístico entre fases — script novo `pnpm db:reset` (`scripts/db-reset.mjs`: `DROP SCHEMA public CASCADE` → `CREATE SCHEMA public` → `pnpm migrate` → `pnpm db:seed:minimal`, guard fail-closed por construção: só host local E nome `teqo(_)?_test`, sem escape `ALLOW_REMOTE_DB` — um DROP não pode herdar o override remoto dos seeds) — e **um build por run** quando build+e2e rodam juntos (o e2e continua servindo o `.next-e2e`; o build default só roda quando é o único build da run; no `verify` do deploy ele saiu de vez, pois o e2e full sempre roda lá). Contrato do nome `_test` extraído para `TEST_DATABASE_NAME_RE` em `scripts/lib/cli.mjs` (fonte única com `assertTestDatabase.ts`); `scripts/db-reset.mjs` entra no `HIGH_RISK_EXACT` **e** no `BUILD_SURFACE_EXACT` (um PR só no reset arma o próprio step no CI), `scripts/lib/cli.mjs` entra no `HIGH_RISK_EXACT`; o mirror local `gate:ci` ganha o reset antes do build (paridade CI↔local, mesma condição do YAML); `deploy` no homeserver intocado (migrate-before-build OPS66 preservado — o migrate embutido do build vira no-op aceito). Docs canônicos (`TESTING.md`, `AGENT-OPS.md`) atualizados para o setup único. Contabilização honesta: ciclos de estado 4→2, Postgres 2→1, builds 2→1, migrates executados 4→3 (residual = no-op embutido), seeds 2→2. Verificação: guards fail-closed (3 recusas), reset sobre resíduo real (tabela junk somada → baseline 435 municípios + seed minimal de volta), idempotência (re-execução), suite int **full** verde pós-reset (830 tests); `pnpm lint`/`tsc`/`format:check`/`knip`/`check:cycles`/`test:unit` (2445) verdes. + **OPS88 — verify do CI (PR e deploy): um Postgres com reset determinístico, um build, migrate/seed sem repetição (2026-08-24):** o `checks` do PR e o `verify` do deploy reconstruíam o mesmo estado até 4× por run (dois containers Postgres `postgres-int`/`postgres-build`, dois migrates explícitos, dois seeds, dois builds — cada build re-rodando migrate embutido; o build default `.next` nem era servido por nada). Agora: **um Postgres por run** com reset determinístico entre fases — script novo `pnpm db:reset` (`scripts/db-reset.mjs`: `DROP SCHEMA public CASCADE` → `CREATE SCHEMA public` → `pnpm migrate` → `pnpm db:seed:minimal`, guard fail-closed por construção: só host local E nome `teqo(_)?_test`, sem escape `ALLOW_REMOTE_DB` — um DROP não pode herdar o override remoto dos seeds) — e **um build por run** quando build+e2e rodam juntos (o e2e continua servindo o `.next-e2e`; o build default só roda quando é o único build da run; no `verify` do deploy ele saiu de vez, pois o e2e full sempre roda lá). Contrato do nome `_test` extraído para `TEST_DATABASE_NAME_RE` em `scripts/lib/cli.mjs` (fonte única com `assertTestDatabase.ts`); `scripts/db-reset.mjs` entra no `HIGH_RISK_EXACT` (harness de teste) e o mirror local `gate:ci` ganha o reset antes do build (paridade CI↔local); `deploy` no homeserver intocado (migrate-before-build OPS66 preservado — o migrate embutido do build vira no-op aceito). Contabilização honesta: ciclos de estado 4→2, Postgres 2→1, builds 2→1, migrates executados 4→3 (residual = no-op embutido), seeds 2→2. Verificação: guards fail-closed (3 recusas), reset sobre resíduo real (tabela junk somada → baseline 435 municípios + seed minimal de volta), idempotência (re-execução), suite int **full** verde pós-reset (830 tests); `pnpm lint`/`tsc`/`format:check`/`knip`/`check:cycles`/`test:unit` (2445) verdes. **OPS83 — desbloquear deploy: estabilizar os e2e do verify sem reduzir cobertura; AgendaFeed volta a rodar em prod (2026-08-24):** o deploy manual (`deploy.yml`) estava bloqueado pelo job `verify` (run #15: 1 failed + 7 flaky + 2 skipped + 1 did-not-run). Causas estudadas e corrigidas por reescrita de specs (settle/poll/hermeticidade/escopo de locator) — **nenhum** `test.flaky`, nenhuma remoção de cobertura, nenhuma mudança de workers/retries, zero mudança de app/contrato/acesso: (1) kill switch da home — o poll de server-HTML convergia enquanto a seção renderizada ainda mostrava o link do Instagram; passou a pollar o **DOM rendered** escopado a `[data-home-section="contents"]` (`expectSectionCardCount`, usado também no sibling de fallback IG); (2) section-hidden — o revalidate `tag=posts` era engolido por `.catch`; agora o POST é **assertado** e a convergência ganha orçamento explícito (não bump cego); (3) SavedFilters strict-mode — o link 'Municípios' agora é escopado ao container `[data-slot="sidebar"]` (2º match = bottom nav, #669 absorvida); (4) People — gate `S:` + settle do stream antes de asserir rows (testes 15, pós-search e C118); (5) Municípios:642 — poll DOM da liderança específica em vez de `.first()` no stream; (6) Municípios:386 B176 — `settleMunicipalityStream` entre remove-chip e o próximo clique de opção; (7) Newsletter:222 — `toHaveURL(/#novidades/)` sem `$` (o `?e2e=scroll-…` é legítimo); (8) DemandVisibility:62 — `.goto` com cache-buster para a asserção de 404 do peer (a cache RSC servia a demanda acessível após a troca de cookie). **AgendaFeed (Decision B):** os 2 `test.skip(isProdMode)` foram **removidos** — o build e2e agora inlina `NEXT_PUBLIC_SITE_URL=https://feed.e2e.teqo.test` (`.test` TLD, passa o fail-closed `requireProductionDNSOrigin`), o diálogo gera o link canônico e os testes asserem o **path** e buscam via `campaign.baseURL + path`; confirmado que share usa `window.location.origin`, webauthn é desacoplado via request-host e webhooks usam `campaign.baseURL` — nenhum outro e2e depende do origin. Escopo config: `playwright.config.ts` (default do webServer env em prod), `deploy.yml` e `ci-pr.yml` (env do build-for-e2e + do passo de testes). Verificação: specs afetadas verdes em dev mode (2×) e AgendaFeed verde em `E2E_PROD=1`; `pnpm lint`/`tsc`/`format:check`/`knip`/`check:cycles`/`test:unit` (2443) verdes; PR em main com auto-merge. diff --git a/docs/TESTING.md b/docs/TESTING.md index f1a06a4d..38d3b48b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -10,7 +10,7 @@ Three layers, all local-database-only (see `local-database` skill; tests refuse **Level preference (pyramid):** unit first (pure logic) → integration for Payload/DB boundaries → e2e only when the flow has real benefit the lower layers don't cover (critical user path, UX/navigation, URL contract). Canonical definition: `test-driven-development` skill. -CI (`ci-pr.yml`) is a **single sequential job `checks`** (OPS62 X1): guards → lint (zero warnings) → format → typecheck → knip → cycles → unit/int (**affected by the PR diff**: `vitest --changed` against the PR base via `scripts/test-affected.mjs`, full-suite fallback on high-risk paths) → build → e2e (manifest path→spec via `scripts/e2e-affected.mjs` + `scripts/lib/e2e-affected-manifest.mjs`, blocking; **only in mode `selected` — OPS72: the PR never runs full e2e**; production build + `list`/`github` reporters in CI) — against two isolated Postgres 17 services (int / build+e2e) with **`db:seed:minimal`**. Fail-fast is structural: a failed step stops the run; e2e runs with `PLAYWRIGHT_WORKERS=4`. There is no main verifier (`ci.yml` eliminated in OPS71): the full net including e2e runs in the deploy `verify` job before publishing (manual dispatch). +CI (`ci-pr.yml`) is a **single sequential job `checks`** (OPS62 X1): guards → lint (zero warnings) → format → typecheck → knip → cycles → unit/int (**affected by the PR diff**: `vitest --changed` against the PR base via `scripts/test-affected.mjs`, full-suite fallback on high-risk paths) → build → e2e (manifest path→spec via `scripts/e2e-affected.mjs` + `scripts/lib/e2e-affected-manifest.mjs`, blocking; **only in mode `selected` — OPS72: the PR never runs full e2e**; production build + `list`/`github` reporters in CI) — against ONE Postgres 17 service, with a deterministic **`pnpm db:reset`** (drop schema → migrate → seed:minimal) between the int phase and the build/e2e phase (OPS88). Fail-fast is structural: a failed step stops the run; e2e runs with `PLAYWRIGHT_WORKERS=4`. There is no main verifier (`ci.yml` eliminated in OPS71): the full net including e2e runs in the deploy `verify` job before publishing (manual dispatch). **Local e2e (OPS72 — skill step, discretionary):** before push, run the e2e specs you created + the same affected surface — `pnpm test:e2e:affected` runs `e2e-affected.mjs` against `origin/main`, migrates/seeds, then `pnpm test:e2e` with the selected specs (full suite on high-risk diffs). High-risk diffs diverge from PR CI by design: the PR runs no e2e (selected only, never full) and the local mirror runs the full suite. Forward extra Playwright args after `--` for a narrowed repro. Opt into production mode with `E2E_PROD=1 pnpm test:e2e:affected` (CI and prod mode serve the production build via `pnpm start`; plain local runs use `pnpm dev` — dev-mode flakes like cold-compile timeouts and dev-only console warnings do not reproduce in prod mode). diff --git a/docs/changelog/2026-08-24-ops88.md b/docs/changelog/2026-08-24-ops88.md index 29c2b787..e970a804 100644 --- a/docs/changelog/2026-08-24-ops88.md +++ b/docs/changelog/2026-08-24-ops88.md @@ -1 +1 @@ -**OPS88 — verify do CI (PR e deploy): um Postgres com reset determinístico, um build, migrate/seed sem repetição (2026-08-24):** o `checks` do PR e o `verify` do deploy reconstruíam o mesmo estado até 4× por run (dois containers Postgres `postgres-int`/`postgres-build`, dois migrates explícitos, dois seeds, dois builds — cada build re-rodando migrate embutido; o build default `.next` nem era servido por nada). Agora: **um Postgres por run** com reset determinístico entre fases — script novo `pnpm db:reset` (`scripts/db-reset.mjs`: `DROP SCHEMA public CASCADE` → `CREATE SCHEMA public` → `pnpm migrate` → `pnpm db:seed:minimal`, guard fail-closed por construção: só host local E nome `teqo(_)?_test`, sem escape `ALLOW_REMOTE_DB` — um DROP não pode herdar o override remoto dos seeds) — e **um build por run** quando build+e2e rodam juntos (o e2e continua servindo o `.next-e2e`; o build default só roda quando é o único build da run; no `verify` do deploy ele saiu de vez, pois o e2e full sempre roda lá). Contrato do nome `_test` extraído para `TEST_DATABASE_NAME_RE` em `scripts/lib/cli.mjs` (fonte única com `assertTestDatabase.ts`); `scripts/db-reset.mjs` entra no `HIGH_RISK_EXACT` (harness de teste) e o mirror local `gate:ci` ganha o reset antes do build (paridade CI↔local); `deploy` no homeserver intocado (migrate-before-build OPS66 preservado — o migrate embutido do build vira no-op aceito). Contabilização honesta: ciclos de estado 4→2, Postgres 2→1, builds 2→1, migrates executados 4→3 (residual = no-op embutido), seeds 2→2. Verificação: guards fail-closed (3 recusas), reset sobre resíduo real (tabela junk somada → baseline 435 municípios + seed minimal de volta), idempotência (re-execução), suite int **full** verde pós-reset (830 tests); `pnpm lint`/`tsc`/`format:check`/`knip`/`check:cycles`/`test:unit` (2445) verdes. +**OPS88 — verify do CI (PR e deploy): um Postgres com reset determinístico, um build, migrate/seed sem repetição (2026-08-24):** o `checks` do PR e o `verify` do deploy reconstruíam o mesmo estado até 4× por run (dois containers Postgres `postgres-int`/`postgres-build`, dois migrates explícitos, dois seeds, dois builds — cada build re-rodando migrate embutido; o build default `.next` nem era servido por nada). Agora: **um Postgres por run** com reset determinístico entre fases — script novo `pnpm db:reset` (`scripts/db-reset.mjs`: `DROP SCHEMA public CASCADE` → `CREATE SCHEMA public` → `pnpm migrate` → `pnpm db:seed:minimal`, guard fail-closed por construção: só host local E nome `teqo(_)?_test`, sem escape `ALLOW_REMOTE_DB` — um DROP não pode herdar o override remoto dos seeds) — e **um build por run** quando build+e2e rodam juntos (o e2e continua servindo o `.next-e2e`; o build default só roda quando é o único build da run; no `verify` do deploy ele saiu de vez, pois o e2e full sempre roda lá). Contrato do nome `_test` extraído para `TEST_DATABASE_NAME_RE` em `scripts/lib/cli.mjs` (fonte única com `assertTestDatabase.ts`); `scripts/db-reset.mjs` entra no `HIGH_RISK_EXACT` **e** no `BUILD_SURFACE_EXACT` (um PR só no reset arma o próprio step no CI), `scripts/lib/cli.mjs` entra no `HIGH_RISK_EXACT`; o mirror local `gate:ci` ganha o reset antes do build (paridade CI↔local, mesma condição do YAML); `deploy` no homeserver intocado (migrate-before-build OPS66 preservado — o migrate embutido do build vira no-op aceito). Docs canônicos (`TESTING.md`, `AGENT-OPS.md`) atualizados para o setup único. Contabilização honesta: ciclos de estado 4→2, Postgres 2→1, builds 2→1, migrates executados 4→3 (residual = no-op embutido), seeds 2→2. Verificação: guards fail-closed (3 recusas), reset sobre resíduo real (tabela junk somada → baseline 435 municípios + seed minimal de volta), idempotência (re-execução), suite int **full** verde pós-reset (830 tests); `pnpm lint`/`tsc`/`format:check`/`knip`/`check:cycles`/`test:unit` (2445) verdes. diff --git a/docs/plans/ops88-ci-verify-setup-unico-impl.md b/docs/plans/ops88-ci-verify-setup-unico-impl.md index 6ecb619b..b9de7286 100644 --- a/docs/plans/ops88-ci-verify-setup-unico-impl.md +++ b/docs/plans/ops88-ci-verify-setup-unico-impl.md @@ -1,6 +1,6 @@ # Impl: Verify do CI (PR e deploy): um Postgres com reset determinístico, um build, migrate/seed sem repetição -Status: rascunho +Status: aprovado (gate humano 2026-08-24) Atualizado em: 2026-08-24 Issue: #834 Intenção: docs/plans/ops88-ci-verify-setup-unico.md @@ -82,7 +82,7 @@ flowchart LR - **`.github/workflows/ci-pr.yml`**: remove `postgres-build` + steps 5433; adiciona step "Reset database (single service)" com o `if:` de D2; ajusta `if:` do build default; atualiza comentário de serviços. - **`.github/workflows/deploy.yml`** (job `verify`): espelho do acima, sem skips — remove `postgres-build`, remove build default, adiciona reset entre int e build `.next-e2e`. - **`scripts/gate-ci.mjs`**: `pnpm db:reset` antes 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'` — mudança no reset é harness de teste, deve rodar suíte cheia. +- **`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 classificaria `build_mode='none'` e o próprio step Reset pularia no CI (achado do simplify). - **Docs:** entrada em `docs/changelog/-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). diff --git a/scripts/db-reset.mjs b/scripts/db-reset.mjs index 342f7070..4c6928b3 100644 --- a/scripts/db-reset.mjs +++ b/scripts/db-reset.mjs @@ -52,7 +52,13 @@ if (!LOCAL_HOSTS.has(parsedUrl.hostname)) { ) } -const databaseName = decodeURIComponent(parsedUrl.pathname.replace(/^\//, '')) +let databaseName +try { + databaseName = decodeURIComponent(parsedUrl.pathname.replace(/^\//, '')) +} catch { + die('DATABASE_URL database name has invalid percent-encoding.') +} + if (!TEST_DATABASE_NAME_RE.test(databaseName)) { die( `Refusing to reset database "${databaseName}" — the name must match ` + @@ -60,18 +66,22 @@ if (!TEST_DATABASE_NAME_RE.test(databaseName)) { ) } -const pool = new pg.Pool({ connectionString: databaseUrl, max: 1 }) try { - console.log(`[db:reset] Dropping schema public on "${databaseName}"…`) - await pool.query('DROP SCHEMA public CASCADE') - await pool.query('CREATE SCHEMA public') - console.log('[db:reset] Schema rebuilt — running migrate…') -} finally { - await pool.end() + const pool = new pg.Pool({ connectionString: databaseUrl, max: 1 }) + try { + console.log(`[db:reset] Dropping schema public on "${databaseName}"…`) + await pool.query('DROP SCHEMA public CASCADE') + await pool.query('CREATE SCHEMA public') + console.log('[db:reset] Schema rebuilt — running migrate…') + } finally { + await pool.end() + } + + execFileSync('pnpm', ['migrate'], { stdio: 'inherit' }) + console.log('[db:reset] Migrations applied — seeding minimal baseline…') + execFileSync('pnpm', ['db:seed:minimal'], { stdio: 'inherit' }) +} catch (error) { + die(`reset failed: ${error?.message ?? error}`) } -execFileSync('pnpm', ['migrate'], { stdio: 'inherit' }) -console.log('[db:reset] Migrations applied — seeding minimal baseline…') -execFileSync('pnpm', ['db:seed:minimal'], { stdio: 'inherit' }) - console.log('[db:reset] ✓ deterministic baseline ready (migrate + db:seed:minimal).') diff --git a/scripts/gate-ci.mjs b/scripts/gate-ci.mjs index f437a338..953d084e 100644 --- a/scripts/gate-ci.mjs +++ b/scripts/gate-ci.mjs @@ -125,7 +125,8 @@ const main = async () => { } // --- Phase 2 (expensive) — only when phase 1 would have been green --- - const needsDb = scope.test.mode !== 'none' || scope.build.mode !== 'none' + const needsDb = + scope.test.mode !== 'none' || scope.build.mode !== 'none' || scope.e2e.mode === 'selected' if (needsDb) { console.log('\n[gate:ci] ▶ preflight (teqo_test)') const dbOk = await diagnoseDatabaseTarget({ @@ -156,13 +157,15 @@ const main = async () => { console.log('\n[gate:ci] ⊘ test:int skipped (no src/tests blast radius)') } - // OPS88: mirror of the CI single-Postgres reset — the build phase starts - // from the same clean baseline (drop schema → migrate → seed:minimal) the - // e2e build would get in CI, instead of int fixture residue. - if (scope.build.mode === 'none' && scope.e2e.mode !== 'selected') { - console.log('\n[gate:ci] ⊘ db:reset skipped (no build/e2e phase)') - } else { + // OPS88: mirror of the CI single-Postgres reset — the build phase below + // starts from the same clean baseline (drop schema → migrate → seed:minimal) + // the e2e build gets in CI, instead of int fixture residue. Same condition + // as the CI step. (The gate still builds the default `.next`, not + // `.next-e2e` — e2e never runs here, OPS72.) + if (scope.build.mode !== 'none' || scope.e2e.mode === 'selected') { run('db:reset', 'pnpm', ['db:reset'], dbEnv) + } else { + console.log('\n[gate:ci] ⊘ db:reset skipped (no build/e2e phase)') } if (scope.build.mode === 'none') { diff --git a/scripts/lib/cli.mjs b/scripts/lib/cli.mjs index 3fd04834..51df0618 100644 --- a/scripts/lib/cli.mjs +++ b/scripts/lib/cli.mjs @@ -31,10 +31,12 @@ export const loadCliEnv = () => { /** * Local database hosts — docker-compose service name, the job-network service - * names of the Forgejo-era runner (OPS62 X1: services by name, no host publish — - * the GitHub hosted CI reaches services on published localhost ports, OPS71), - * and the bridge gateway (OPS50: legacy per-port publish kept in the - * allowlist). THE one list. + * names of the Forgejo-era runner (OPS62 X1: services by name, no host + * publish — the GitHub hosted CI reaches services on published localhost + * ports, OPS71; `postgres-int` remains active, `postgres-build` is a + * defensive legacy entry since OPS88 removed the second container), and the + * bridge gateway (OPS50: legacy per-port publish kept in the allowlist). + * THE one list. */ export const LOCAL_HOSTS = new Set([ 'localhost', diff --git a/scripts/lib/test-affected-core.mjs b/scripts/lib/test-affected-core.mjs index b641b8b0..ecf21b43 100644 --- a/scripts/lib/test-affected-core.mjs +++ b/scripts/lib/test-affected-core.mjs @@ -42,6 +42,7 @@ export const HIGH_RISK_EXACT = new Set([ 'scripts/vitest-changed-or-full.mjs', 'scripts/ci-scope.mjs', 'scripts/db-reset.mjs', + 'scripts/lib/cli.mjs', 'scripts/check-test-locations.mjs', 'scripts/check-plans-only-pr-closes.mjs', 'scripts/lib/plansOnlyClosesGuard.mjs', @@ -90,6 +91,9 @@ const BUILD_SURFACE_EXACT = new Set([ 'src/payload.config.ts', 'src/payload-types.ts', 'tsconfig.json', + // OPS88: a change to the reset script must exercise it — build_mode='build' + // arms the Reset step in ci-pr.yml (and the mirror in gate-ci.mjs). + 'scripts/db-reset.mjs', ]) const BUILD_SURFACE_PREFIXES = [ diff --git a/tests/helpers/assertTestDatabase.ts b/tests/helpers/assertTestDatabase.ts index d238de2b..8efcbf67 100644 --- a/tests/helpers/assertTestDatabase.ts +++ b/tests/helpers/assertTestDatabase.ts @@ -9,10 +9,11 @@ * looks test-like. */ -// Self-hosted Forgejo runner: services are reached by NAME on the job network -// (postgres-int / postgres-build, OPS62 X1) or, on the legacy per-port -// publish, through the bridge gateway IP (RFC1918). Same source as the CLI -// guard so both guards admit exactly the same hosts (OPS50/OPS62). +// `postgres-int`/`postgres-build` are legacy Forgejo job-network service +// names (OPS62 X1), kept in the allowlist defensively — since OPS88 only one +// service (`postgres-int`) exists, reached at localhost:5432 on hosted +// runners. Same source as the CLI guard so both guards admit exactly the +// same hosts (OPS50/OPS62). // TEST_DATABASE_NAME_RE is the shared name contract (cli.mjs) — the same // regex `scripts/db-reset.mjs` uses before dropping the schema (OPS88). import { defaultGatewayHost, TEST_DATABASE_NAME_RE } from '../../scripts/lib/cli.mjs' diff --git a/tests/unit/localHosts.unit.spec.ts b/tests/unit/localHosts.unit.spec.ts index 00d45dcb..c924ee0a 100644 --- a/tests/unit/localHosts.unit.spec.ts +++ b/tests/unit/localHosts.unit.spec.ts @@ -11,7 +11,7 @@ describe('LOCAL_HOSTS (OPS50/OPS62 — self-hosted runner services by name, lega } }) - it('admits the job-network service names (OPS62 X1: no host publish)', () => { + it('keeps the legacy job-network service names in the allowlist (OPS62 X1; postgres-build dormant since OPS88)', () => { expect(LOCAL_HOSTS.has('postgres-int')).toBe(true) expect(LOCAL_HOSTS.has('postgres-build')).toBe(true) })