teqo-backup/AGENTS-infra.md
Francisco Solla 067857a03a fix(OPS124): desabilita o plugin Payload do knip e explicita as exclusões (Closes #1094)
O `pnpm knip` imprimia `ERROR: Error loading src/payload.config.ts
(...server-only...)` e saía 0 — gate verde com o ERROR silencioso, no CI
(`ci-pr.yml`/`deploy.yml`) e local. O plugin embutido do Payload carregava
`src/payload.config.ts` via jiti sob as condições Node padrão e o import
transitivo de `server-only` lançava (só o `react-server` traz o `empty.js`);
o `paths.server-only` do `knip.json` alimenta apenas o resolver estático, não
o carregamento jiti (e o `tsconfig` alias, que funcionaria no load, foi
rejeitado por re-apontar a guarda server/client — classe OPS69).

Fix: `"payload": false` e as contribuições do plugin explicitadas no
`knip.json` (`!src/payload-types.ts` load-bearing; `!src/migrations/**`
paridade), removendo o bloco `paths` inerte. Provado: `pnpm knip` sai 0 sem o
ERROR, um export morto plantado é detectado, e a corrida de controle com
`server-only` neutralizado dá findings idênticos.

Plano: docs/plans/ops-knip-importmap-artifact-impl.md
2026-09-16 19:55:07 -03:00

29 lines
15 KiB
Markdown

# Infrastructure layer (AGENTS layer — load when touching deploy, CI, or local dev)
## Deploy & CI
Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). **CI/PR/deploy AND the ISSUE TRACKER live on GitHub (OPS71 + OPS76, 2026-08-21)** (`github.com/fsolla/teqo/issues` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips talk to the GitHub API; GitHub is the single host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado, skips via `ci-scope.mjs`); its check-run `CI (PR) / checks` is the required check of `main`. **There is NO main verifier (`ci.yml`) anymore**: a merge to `main` starts the deploy run automatically (OPS104 supersedes the OPS102 manual-only anti-goal; `workflow_dispatch` stays as the manual escape) — the verifier IS the deploy's `verify` job. The deploy workflow has five jobs: `preflight` (hosted; `scripts/deploy-preflight.mjs` skips the run when another deploy is `queued`/`in_progress` — a `waiting` production approval does not count) → `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full, rodado uma vez por commit) → `deploy-staging` (`environment: staging`, OPS103) → `requeue` (hosted, outside the deploy concurrency group; `scripts/deploy-requeue.mjs` re-dispatches a fresh run when `main` advanced — OPS104) and `deploy-production` (`environment: production` com required reviewer, `needs: [deploy-staging]`; OPS107: sem `concurrency` — o approval pendente não segura o staging nem a aprovação de runs novos, então aprove só o run escolhido e rejeite os demais): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). O script é único e parametrizado por `TEQO_ENV` (`production` default intocado; `staging` = `teqo-staging`/`teqo_staging`/`~/stack/teqo-staging.env`/`127.0.0.1:1314`, bucket `teqo-media-staging`; envs de staging só no homeserver, nunca como GitHub secrets; staging não é indexável). The remote Docker deployment is gone; the production container runs the monolith directly. The Docker Compose file is used ONLY for local dev databases. The `run-deploy.yml` (OPS38/OPS46) was deleted in OPS71. There is no `run-deploy.yml` or equivalent in GitHub Actions — deploy is `deploy.yml` only.
**Auto-unblock do `verify` (OPS106):** um `verify` vermelho de push em `main` dispara UM agente autônomo de desbloqueio no homeserver — workflow separado `.github/workflows/auto-unblock.yml` (`workflow_run` no Deploy, push-only, conferindo pela jobs API que o job `verify` falhou), `scripts/auto-unblock.sh` (flock `/tmp/teqo-unblock.lock`) + `scripts/auto-unblock.mjs` (token Issue `auto-unblock` = single-flight; falha com agente ativo só comenta) e `scripts/auto-unblock-agent.mjs` destacado (`detached`, fd 9 herdado, sem `RUNNER_TRACKING_ID`; cap 4h) que provisiona `fix/*` via `scripts/worktree.mjs fix --headless`, aplica o guard de banco local `teqo_wt*`/`*_test` e roda `opencode run --command bug-fix`. O agente faz push/PR com o `AUTOMERGE_PAT` por credential helper não-persistente; `deploy.yml`/`verify` continuam intocados (sem auto-retry, produção segue com approval humano). Bootstrap + operação: `docs/ops/teqo-1313-deploy.md` (seção Auto-unblock); smoke `pnpm unblock:check`.
**Guarding `pnpm push` (OPS24+):** every PR push goes through `scripts/git-push.mjs`, which runs `ensure-repo-deps` (install + Husky in worktrees — OPS51), then `gate:push` (the pre-push hook), which chains `gate:ci` — a serialized mirror of the CI `checks` job (**without e2e**: OPS72 moved e2e to an explicit skill step) — plus the `docs-guards` (changelog entries additions-only + frozen HISTORY snapshot + uncommitted regenerable aggregate — OPS63/OPS85; conflict markers; the rewrite escape stays in CI only). The hook blocks `git push` on failure; escape: `git push --no-verify` (WIP only). There is no YAML `push` gate — the hook is the gate. **CI red on your PR is yours**, even pre-existing or infra — "out of feature scope" is not a stop condition (policy "Dono do PR, dono do CI" in `docs/AGENT-OPS.md`).
## Database & local development
- Local dev runs against a local Docker Postgres (`pnpm db:start`). To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data).
- `pnpm db:doctor` diagnoses local database connectivity for the dev AND test targets: it names who holds the port, points at exited/crash-looping containers, and flags Postgres containers sharing one data volume (two postmasters on one PGDATA corrupt the WAL — this destroyed the local cluster's checkpoint on 2026-07-25; the canonical setup is ONE compose container on 5432 hosting `teqo` + `teqo_test`). `pnpm dev` runs the same preflight via `guard-dev-db` and refuses to start with a named remedy instead of a later ECONNREFUSED. `db:start`/`db:stop` pin the compose project with `-p teqo`, so they always target that ONE container even from a worktree directory. **`db:start` is idempotent (OPS37):** with `teqo-postgres-1` up and healthy it is a no-op — a plain `compose up` from another worktree would recreate the shared container and kill every live dev/e2e connection (the compose file's relative `./docker/postgres/init` bind mount makes the config hash differ per worktree); apply a real config/image change on purpose with `pnpm db:start --force-recreate`.
- **Per-worktree environments (`pnpm worktree next`, 2026-08-08; default-go 2026-08-10):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. **`next` claims BEFORE creating the worktree (OPS33, 2026-08-10):** the same queue/order and optimistic lock as `pnpm agent:claim` (shared `claimIssue` in `scripts/lib/agent-github.mjs`); a failed claim prints the reason and stops with no orphan worktree; `--issue <N>` claims the directed issue (`ready`) or **reopens** an already-claimed one (`in-progress`, no re-claim, worktree reuse stays idempotent); the claim brief is printed and the output warns "já claimada — não rodar `pnpm agent:claim"` (`plan`/`new`/`kill` never touch Issues; the pool's coordinated `claimIssueForPool` is untouched). `worktree next [--issue N] [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, capped at 999 with hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt<slot>` (dev) and `teqo_wt<slot>_test` (tests) created on the shared container with the full migration chain AND the minimal seed applied (`pnpm migrate` + `pnpm db:seed:minimal` per database — OPS28 parity: CI runs the same pair before e2e, so a worktree's `pnpm test:e2e` mirrors CI instead of failing on seed-dependent specs; the seed is idempotent and local-guarded, and `--no-migrate` skips both); writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/`S3_*` (all-or-nothing)/VAPID from the main repo — plus the gitignored `.opencode/secrets/*` (e.g. `penpot-token`, referenced by the repo's `opencode.json` penpot/postgres MCPs — gated to subagents `penpot`/`postgres` via `permission`), without which opencode refuses to launch ("bad file reference" for the dangling `{file:…}`). **`next`/`plan`/`new` print `cd <dir>` as the last line BY DEFAULT** — the caller applies it... (line truncated to 2000 chars) **OPS110 (sessões persistentes):** no terminal, `next`/`plan`/`new`/`fix` delegam o launch a `scripts/agent-session.mjs` — o run vive no `opencode serve` compartilhado (loopback `127.0.0.1:4199`, `TEQO_AGENT_SERVER_PORT`) com driver destacado (`--auto`) e TUI anexável (`opencode attach -s <sessionID>`); fechar o terminal não encerra (encerrar é `pnpm agent:session stop`; `worktree kill` encerra a sessão do worktree antes de removê-lo) e `list --json` é o endereçamento do OPS109; bind fora do loopback exige `OPENCODE_SERVER_PASSWORD` (fail-closed, sem `--mdns`).
- To (repopulate news content, `pnpm db:seed:posts` fetches articles live from jorgesolla.com.br into the LOCAL db (same non-local `DATABASE_URL` guard as `pnpm dev`). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the production `posts` cache via `POST /api/revalidate`. See "Posts & Tags" in `AGENTS-public.md`.
- To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard). Since the 2026-07-23 hardening, elections-tab reads are cached under the `election-tse` tag — after re-seeding a database the deployed app reads, bust it via `POST /api/revalidate?tag=election-tse`. See "Election baseline data" in `AGENTS-campaign.md`.
## Local verification and deploy checklist
1. Confirm `DATABASE_URL` is `postgresql://teqo:teqo@localhost:5432/teqo` and `.env.test` points to `teqo_test`; never use the homeserver prod DB for development, tests, E2E, or local builds. In a provisioned worktree (`pnpm worktree next`) the values come from its `.env.local` / `.env.test.local` (`teqo_wt<slot>` / `teqo_wt<slot>_test`, dev port `3100+slot`) — read them there instead of assuming the main-repo names.
2. Run `pnpm db:start`, `pnpm migrate:status`, and `DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test pnpm migrate:status`; every migration must show `Ran: Yes`.
3. Run `pnpm generate:types`, `pnpm generate:importmap` when components changed — **com as envs `S3_*` setadas (valores dummy ok) enquanto o plugin `s3Storage` for condicional: sem elas o importMap fica órfão do `S3ClientUploadHandler` e o admin abre em branco em produção (OPS69, P0 em 2026-08-19)** —, `pnpm exec tsc --noEmit`, `pnpm lint` (zero warnings — the script enforces `--max-warnings=0`), `pnpm format:check` (Prettier is the formatting authority; `pnpm format` fixes), `pnpm exec knip` (dead files/dependencies are CI-blocking; delete what your change orphaned), `pnpm check:cycles` (madge — zero circular imports), `pnpm test` (unit + int), and `pnpm build` against the local database. Payload CLI scripts (`migrate`, `generate:*`, `payload`) and the build migrate step use `--conditions=react-server` so `server-only` modules load correctly outside Next.js. **Política de e2e (OPS72 + OPS86):** o e2e **não roda no gate local** (`gate:push`) — é passo **explícito da skill** (`work-issue`/`agent-work-issue`): antes do push o agente roda localmente os e2e que **criou** + os da **mesma superfície afetada** — **discricionário** (o agente decide quais; ferramenta: `pnpm test:e2e:affected`, espelho local do classifier — gera `--no-deps` para runs filtrados, S6-FOLLOWUP: no dev mode a cadeia de dependencies dos projects arrasta os arquivos das dependências via closure; filtros manuais locais: `pnpm test:e2e --no-deps -- <path>` ou `--no-deps --project=<família>`; limitação #72: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos). O **CI de PR roda apenas o blast radius** (via `ci-scope.mjs` — **nunca `full`**): diff mapeado roda `selected`, diff **high-risk** (coleções/globals/migrações/lockfile/harness) roda o **`curated`** — conjunto fixo de 5 specs (`campaignPermissionProfile`, `campaignDemandVisibility`, `campaignAiTranscribe`, `campaignAgendaFeed`, `campaignNewsletter`, congelado por pin em `e2eAffectedManifest.unit.spec.ts`) — **nunca zero e2e no PR**; arquivo de **área de risco sem entry no manifesto** (`src/utilities/access`, `src/lib/schemas`, `campaignPushClient`, `src/utilities/ai`) classifica **`unmapped-risk` e o PR FALHA** listando os arquivos (fail-closed; resolver = adicionar entry no `E2E_AFFECTED_MANIFEST`); src/ não-mapeado não-risco roda o smoke `campaignHomeActions`. Os passos unit/int `changed` passam por `scripts/vitest-changed-or-full.mjs`: seleção vazia cai em **suíte full** — nunca "0 testes, verde". O e2e **full** roda no `verify` do deploy (o run nasce no merge — OPS104; a produção só publica com o approve do reviewer — gateia merge e deploy, rede de segurança final antes de publicar); localmente, diff high-risk segue rodando full no `pnpm test:e2e:affected`.
- Never run `knip --fix` blind: knip's module graph is deliberately scoped (`"payload": false` — OPS124 removed the Payload plugin because it could not load `payload.config.ts`: `server-only`'s default entry throws under knip's non-RSC loader, and aliasing it would mean re-pointing `server-only` in the shared `tsconfig.json`, rejected as an OPS69-class risk), so `--fix` can delete exports that migrations/tests still reference. Verify each removal (e.g. `git grep -w <symbol>`) before deleting.
- Run gate commands bare — never piped (`pnpm test | tail` swallows the exit code, and a real failure was masked exactly this way during Pass 2). Large outputs are captured by the terminal; read them from there.
4. Scan every edited runtime source file with Aikido and resolve new findings before handoff. Do not expand hardening work into unrelated pre-existing findings without an explicit scope change.
5. Review production environment variables and migration SQL before deploy; `pnpm build` applies pending migrations to the deployed database automatically. **`NEXT_PUBLIC_SITE_URL` (ex.: `https://jorgesolla1313.com.br`) é obrigatória em Production** — sem ela, `getCampaignInviteBaseURL()` lança em runtime e o fluxo de convites/webauthn/link de import da agenda falha com mensagem genérica (C98, 2026-08-09: produção ficou sem a env e "gerar link" do feed quebrava; o preview tinha a de stage). Em staging (OPS103) a env carrega `https://staging.jorgesolla1313.com.br` — o mesmo fail-closed vale e a origem `staging.` também desliga o index (`robots.ts` + metadata).
6. **Mandatory production blocker:** do not insert real leadership/support data or enable invites until electoral counsel documents the LGPD art. 11 basis, approves the specific versioned consent text, and an admin creates it with exactly `Consent.key = 'lideranca-autopreenchimento'`. The app intentionally fails closed when this key is absent.
7. **Remodel deploy (historical — applied to production 2026-07-23):** the destructive remodel chain (`20260721_020109_remodel_municipalities` → `20260723_200000_remodel_municipalities` + `20260723_202000_reconcile_municipality_remodel`) already ran in production via the Vercel build: campaign vertical reset, 435 municípios seeded, public-site data untouched. Treat any future destructive migration the same way — review its SQL before the first production build that includes it.