teqo-backup/AGENTS-infra.md
Francisco Solla 119b8980fb
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
fix(OPS107): aprovação de produção não ocupa a lane do staging (Closes #1002)
2026-09-14 08:18:21 -03:00

14 KiB

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)
  • 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: when knip's module graph is incomplete (it still cannot load payload.config.ts — ledgered as P3), --fix deletes 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.