12 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: publishing is a manual workflow_dispatch (.github/workflows/deploy.yml) with two jobs — verify (hosted ubuntu-latest, suíte full incl. e2e full) → deploy (needs: [verify], runs-on: [self-hosted, homeserver]): 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). 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.
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, usepnpm db:pull(scripts/db-pull.mjs): it only reads prod, only writes local, and excludes supporter PII (contact/signature/subscriptionrow data). pnpm db:doctordiagnoses 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 hostingteqo+teqo_test).pnpm devruns the same preflight viaguard-dev-dband refuses to start with a named remedy instead of a later ECONNREFUSED.db:start/db:stoppin the compose project with-p teqo, so they always target that ONE container even from a worktree directory.db:startis idempotent (OPS37): withteqo-postgres-1up and healthy it is a no-op — a plaincompose upfrom another worktree would recreate the shared container and kill every live dev/e2e connection (the compose file's relative./docker/postgres/initbind mount makes the config hash differ per worktree); apply a real config/image change on purpose withpnpm 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 sharedteqo_test, or a second Postgres container.nextclaims BEFORE creating the worktree (OPS33, 2026-08-10): the same queue/order and optimistic lock aspnpm agent:claim(sharedclaimIssueinscripts/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 rodarpnpm agent:claim"(plan/new/killnever touch Issues; the pool's coordinatedclaimIssueForPoolis untouched).worktree next [--issue N] [--stay]provisions: a dev-server port3100+slot(slot = numeric part of the issue code, capped at 999 with hash fallback, bumped on collision among live worktrees), plus dedicated databasesteqo_wt<slot>(dev) andteqo_wt<slot>_test(tests) created on the shared container with the full migration chain AND the minimal seed applied (pnpm migrate+pnpm db:seed:minimalper database — OPS28 parity: CI runs the same pair before e2e, so a worktree'spnpm test:e2emirrors CI instead of failing on seed-dependent specs; the seed is idempotent and local-guarded, and--no-migrateskips both); writes.env.local+.env.test.local(gitignored, marker# teqo worktree env (generated by pnpm worktree next); manual envs are never overwritten) and copiesPAYLOAD_SECRET/S3_*(all-or-nothing)/VAPID from the main repo — plus the gitignored.opencode/secrets/*(e.g.penpot-token, referenced by the worktree'sopencode.jsonMCP config), without which opencode refuses to launch ("bad file reference" for the dangling{file:…}).next/plan/newprintcd <dir>as the last line BY DEFAULT — the caller applies it... (line truncated to 2000 chars) - To (repopulate news content,
pnpm db:seed:postsfetches articles live from jorgesolla.com.br into the LOCAL db (same non-localDATABASE_URLguard aspnpm dev). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the productionpostscache viaPOST /api/revalidate. See "Posts & Tags" inAGENTS-public.md. - To (re)populate the TSE 2022 election baseline,
pnpm db:seed:tsedownloads 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 theelection-tsetag — after re-seeding a database the deployed app reads, bust it viaPOST /api/revalidate?tag=election-tse. See "Election baseline data" inAGENTS-campaign.md.
Local verification and deploy checklist
- Confirm
DATABASE_URLispostgresql://teqo:teqo@localhost:5432/teqoand.env.testpoints toteqo_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 port3100+slot) — read them there instead of assuming the main-repo names. - Run
pnpm db:start,pnpm migrate:status, andDATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test pnpm migrate:status; every migration must showRan: Yes. - Run
pnpm generate:types,pnpm generate:importmapwhen components changed — com as envsS3_*setadas (valores dummy ok) enquanto o plugins3Storagefor condicional: sem elas o importMap fica órfão doS3ClientUploadHandlere 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 formatfixes),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), andpnpm buildagainst the local database. Payload CLI scripts (migrate,generate:*,payload) and the build migrate step use--conditions=react-serversoserver-onlymodules 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-depspara 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 noseedTestUser→--workers=1ou a cadeia padrão de projetos). O CI de PR roda apenas o blast radius (viaci-scope.mjs— nuncafull): diff mapeado rodaselected, diff high-risk (coleções/globals/migrações/lockfile/harness) roda ocurated— conjunto fixo de 5 specs (campaignPermissionProfile,campaignDemandVisibility,campaignAiTranscribe,campaignAgendaFeed,campaignNewsletter, congelado por pin eme2eAffectedManifest.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) classificaunmapped-riske o PR FALHA listando os arquivos (fail-closed; resolver = adicionar entry noE2E_AFFECTED_MANIFEST); src/ não-mapeado não-risco roda o smokecampaignHomeActions. Os passos unit/intchangedpassam porscripts/vitest-changed-or-full.mjs: seleção vazia cai em suíte full — nunca "0 testes, verde". O e2e full roda noverifydo deploy manual (gateia merge e deploy — rede de segurança final antes de publicar); localmente, diff high-risk segue rodando full nopnpm test:e2e:affected.- Never run
knip --fixblind: when knip's module graph is incomplete (it still cannot loadpayload.config.ts— ledgered as P3),--fixdeletes 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 | tailswallows 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.
- Never run
- 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.
- Review production environment variables and migration SQL before deploy;
pnpm buildapplies 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). - 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. - 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.