diff --git a/.forgejo/workflows/ci-pr.yml b/.forgejo/workflows/ci-pr.yml index e8faba1b..1035650f 100644 --- a/.forgejo/workflows/ci-pr.yml +++ b/.forgejo/workflows/ci-pr.yml @@ -16,11 +16,10 @@ # PR and on main (ci.yml), incl. campaignMigrationReconciliation — the check's # hard fail deadlocked every open schema PR the moment two coexisted. # -# Forgejo port (OPS50): Postgres services publish per-port on the runner -# machine and steps reach them through the bridge gateway (DB_HOST from -# /proc/net/route). Host ports derive from the run id (OPS62 X1) so concurrent -# runs never collide; two isolated services — int and build+e2e — keep e2e -# from inheriting int fixture residue (same isolation the former jobs had). +# Forgejo port (OPS50): Postgres services live on the job network and are +# reached by NAME (postgres-int / postgres-build on 5432) — no host publish, +# so concurrent runs never collide on host ports (OPS62 X1). Two isolated +# services keep e2e from inheriting int fixture residue. name: CI (PR) on: @@ -36,19 +35,19 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 50 services: - # Host ports derive from the run id (OPS62 X1): the single job holds - # both services for the whole run, and concurrent runs on the shared - # runner would collide on fixed ports ("port is already allocated") — - # a ~1% residual collision when two runs share run_id % 100 is loud, - # harmless and fixed by the next push. + # Services are reached by NAME on the job network (no host publish — + # OPS62 X1): concurrent runs never collide on host ports, which the + # single long-lived job would hold for the whole run. Two isolated + # services — int and build+e2e — keep e2e from inheriting int fixture + # residue (same isolation the former jobs had). Forgejo 9 does not + # expand expressions in `services.ports`, so a per-run port scheme was + # dropped in favour of the job-network DNS. postgres-int: image: postgres:17-alpine env: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test - ports: - - ${{ 5433 + (github.run_id % 100) }}:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s @@ -60,8 +59,6 @@ jobs: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test - ports: - - ${{ 5533 + (github.run_id % 100) }}:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s @@ -133,16 +130,14 @@ jobs: - name: Unit tests (changed since PR base) if: steps.scope.outputs.test_mode == 'changed' run: pnpm test:unit -- --changed origin/${{ github.base_ref }} --passWithNoTests - - name: Prepare database URL (int service via bridge gateway) + - name: Prepare database URL (int service) run: | set -euo pipefail - DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route) - DB_PORT=${{ 5433 + (github.run_id % 100) }} - echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" >> "$GITHUB_ENV" + echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" >> "$GITHUB_ENV" # vitest re-loads .env.test over the process env, so the service URL # must win through the .env.test.local layer (same mechanism # `pnpm worktree next` uses). OPS50. - echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" > .env.test.local + echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" > .env.test.local - name: Migrate (int database) run: pnpm migrate - name: Seed minimal (int database) @@ -163,14 +158,12 @@ jobs: VITEST_MAX_WORKERS: 4 run: pnpm test:int -- --changed origin/${{ github.base_ref }} --passWithNoTests - - name: Prepare database URL (build/e2e service via bridge gateway) + - name: Prepare database URL (build/e2e service) if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode != 'none' run: | set -euo pipefail - DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route) - DB_PORT=${{ 5533 + (github.run_id % 100) }} - echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" >> "$GITHUB_ENV" - echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" > .env.test.local + echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" >> "$GITHUB_ENV" + echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" > .env.test.local - name: Migrate (build/e2e database) if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode != 'none' run: pnpm migrate diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 810bd150..89fcd44c 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -11,13 +11,10 @@ # pushes it to the local registry (localhost:5000), applies pending migrations # through the compose maintenance service and rolls the container out. # -# Forgejo port (OPS50): Postgres services publish to per-port HOST ports on -# the runner machine (act/forgejo runner semantics — the canonical 5432 is the -# workstation's own dev database), and steps reach them through the bridge -# gateway: DB_HOST = gateway IP from /proc/net/route. Host ports derive from -# the run id (OPS62 X1) so concurrent runs never collide; two isolated -# services — int and build+e2e — keep e2e from inheriting int fixture residue. -# No `permissions:` (Forgejo issues one native GITHUB_TOKEN) and no +# Forgejo port (OPS50): Postgres services live on the job network and are +# reached by NAME (postgres-int / postgres-build on 5432) — no host publish, +# so concurrent runs never collide on host ports (OPS62 X1). No +# `permissions:` (Forgejo issues one native GITHUB_TOKEN) and no # `concurrency:` (unsupported). `gh workflow run` requeues became # `node scripts/forgejo-dispatch.mjs`. name: CI @@ -36,19 +33,18 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 50 services: - # Host ports derive from the run id (OPS62 X1): the single job holds - # both services for the whole run, and concurrent runs on the shared - # runner would collide on fixed ports ("port is already allocated") — - # a ~1% residual collision when two runs share run_id % 100 is loud, - # harmless and fixed by the next push. + # Services are reached by NAME on the job network (no host publish — + # OPS62 X1): concurrent runs never collide on host ports, which the + # single long-lived job would hold for the whole run. Two isolated + # services — int and build+e2e — keep e2e from inheriting int fixture + # residue. Forgejo 9 does not expand expressions in `services.ports`, + # so a per-run port scheme was dropped in favour of the job-network DNS. postgres-int: image: postgres:17-alpine env: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test - ports: - - ${{ 5433 + (github.run_id % 100) }}:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s @@ -60,8 +56,6 @@ jobs: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test - ports: - - ${{ 5533 + (github.run_id % 100) }}:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s @@ -91,16 +85,14 @@ jobs: - name: Unit tests (full suite) run: pnpm test:unit - - name: Prepare database URL (int service via bridge gateway) + - name: Prepare database URL (int service) run: | set -euo pipefail - DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route) - DB_PORT=${{ 5433 + (github.run_id % 100) }} - echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" >> "$GITHUB_ENV" + echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" >> "$GITHUB_ENV" # vitest re-loads .env.test over the process env, so the service URL # must win through the .env.test.local layer (same mechanism # `pnpm worktree next` uses). OPS50. - echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" > .env.test.local + echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" > .env.test.local - name: Migrate (int database) run: pnpm migrate - name: Seed minimal (int database) @@ -114,13 +106,11 @@ jobs: VITEST_MAX_WORKERS: 4 run: pnpm test:int - - name: Prepare database URL (build/e2e service via bridge gateway) + - name: Prepare database URL (build/e2e service) run: | set -euo pipefail - DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route) - DB_PORT=${{ 5533 + (github.run_id % 100) }} - echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" >> "$GITHUB_ENV" - echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" > .env.test.local + echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" >> "$GITHUB_ENV" + echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" > .env.test.local - name: Migrate (build/e2e database) run: pnpm migrate - name: Seed minimal (build/e2e database) diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index a1ead95a..cc47eb4e 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -69,14 +69,14 @@ Arquivos de "lista" que N agentes escrevem em paralelo (changelog, plans, migra ## CI por alvo -| Workflow | Trigger | Banco | Passos | -| ------------------------------------ | -------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `ci-pr.yml` | PR → `main` | 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 e o slot é liberado; e2e 1 processo com `PLAYWRIGHT_WORKERS=4` (2 services por run, portas derivadas do `run_id`) | -| `ci.yml` | push / dispatch `main` | 2× service Postgres 17 | Full sempre; job único `checks` sequencial (mesma mecânica X1); job `deploy` (runs-on `host`) publica no homeserver via `scripts/deploy-homeserver.sh` (OPS53) | -| `issue-done-on-main-merge.yml` | PR merged → `main` | — | `Closes`/`Fixes` → `done` + `in-prod` | -| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` | — | `Related #N` aguardando plano → `ready` (OPS18; soft-skip) | -| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `forgejo-pr-automerge.mjs` (plain Node): espera checks e mergea por rebase; draft `cursor/*` → Ready; draft não-`cursor/*` = veto, skip | -| `agent-pool.yml` | schedule / PR closed `main` / dispatch | — | Supervisor do pool (`POOL_GITHUB_TOKEN`) | +| Workflow | Trigger | Banco | Passos | +| ------------------------------------ | -------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ci-pr.yml` | PR → `main` | 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 e o slot é liberado; e2e 1 processo com `PLAYWRIGHT_WORKERS=4`; 2 services por nome na rede do job (`postgres-int`/`postgres-build`, sem publish host) | +| `ci.yml` | push / dispatch `main` | 2× service Postgres 17 | Full sempre; job único `checks` sequencial (mesma mecânica X1); job `deploy` (runs-on `host`) publica no homeserver via `scripts/deploy-homeserver.sh` (OPS53) | +| `issue-done-on-main-merge.yml` | PR merged → `main` | — | `Closes`/`Fixes` → `done` + `in-prod` | +| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` | — | `Related #N` aguardando plano → `ready` (OPS18; soft-skip) | +| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `forgejo-pr-automerge.mjs` (plain Node): espera checks e mergea por rebase; draft `cursor/*` → Ready; draft não-`cursor/*` = veto, skip | +| `agent-pool.yml` | schedule / PR closed `main` / dispatch | — | Supervisor do pool (`POOL_GITHUB_TOKEN`) | Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5` e `pnpm/action-setup@v6` usam Node 24 nativo — sem `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`. diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index bcd85119..e38e895c 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -44,6 +44,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente **Recently resolved (2026-08-18):** **OPS62** — fail-fast estrutural do CI: `ci-pr.yml` e `ci.yml` deixaram de ter jobs irmãos. Cada workflow agora tem **um único job `checks` sequencial** (ci-pr: guards de conteúdo → lint → format → typecheck → knip → cycles → unit → int → build → e2e, com skips do `ci-scope.mjs` agora como step com outputs; ci.yml: mesma mecânica full + deploy OPS53 intacto), então **step vermelho interrompe o job, o run falha e o slot do runner é liberado imediatamente** — sem API de cancel (o Forgejo 9.0.3 não tem), sem watchdog, sem upgrade do Forgejo no homeserver (decisão X1 do gate de execução substitui o watchdog+upgrade da intenção; o aceite "API de cancel/upgrade" e "mesmos jobs, mesma cascata" foi revisado pelo humano — o que o CI _verifica_ permanece idêntico, só o envelope muda). Diagnóstico preservado: steps têm logs próprios. O job id `checks` mantém o contexto `CI (PR) / checks` que o automerger/`waitForChecks` e a branch protection esperam (OPS61). Paralelismo do e2e recuperado por `PLAYWRIGHT_WORKERS=4` num único processo (substitui os 2 shards × 2 workers; mesma carga); **2 services Postgres isolados** (int e build+e2e) com **portas host derivadas do `run_id`** (`5433/5533 + run_id % 100` — o job único segura as duas portas pelo run inteiro; runs concorrentes colidiriam em portas fixas) preservam a topologia de hoje (e2e nunca herda resíduo de fixtures do int); 1 checkout + 1 `pnpm install` por run (antes ~10). Plumbing morto removido: `e2eShardConfig` e o output `e2e_shards` do classifier (sem consumidor). Guards de conteúdo movidos para o início do pipeline (falham rápido e são PR-scoped). `/simplify` (3 reviewers): P1 aplicados — `set -euo pipefail` no step `scope` (crash do classifier com outputs vazios silenciaria skips → false green) e portas por run (acima); P2 aplicados — pins de invariantes ancorados em chaves YAML (comentários não satisfazem `needs: [checks]`/`matrix:`/`fail-fast:`), pin do wiring crítico `id: scope` + `if: e2e_mode == 'full'` (perdê-lo pula a suíte em silêncio), comentário morto do sharding em `playwright.config.ts` e `docs/TESTING.md` corrigidos (ainda citavam `.github/workflows` e jobs paralelos); P3 — JSDoc do `ci-scope.mjs` e redação do impl plan; o P2 do `waitForChecks` (timeout 30 min vs job 50 min) fica como débito de CI anotado no impl. Docs: tabela CI do AGENT-OPS reescrita + linha morta do `styfle/cancel-workflow-action` (removida no OPS50) apagada. Sem migration/access/Consent/UI. Testes: invariantes novos em `ciSkipInvariants.unit.spec.ts` (um único job `checks`, sem `matrix:`/`fail-fast:`/`runs-on: host` no ci-pr, `PLAYWRIGHT_WORKERS: 4`, guards antes do lint, deploy gated em `needs: [checks]`); `testAffected.unit.spec.ts` sem o describe do shard config. **Validação ao vivo na própria entrega:** o ci-pr deste PR já é o envelope novo — o run verde prova a suíte inteira no job único; a medição do tempo de parede fica registrada no run. Custo aceito (X1): run verde ~21 min esperados (vs ~15–18 com jobs paralelos) e e2e vermelho é o caso mais lento de veredito; débito registrado no impl plan (matrix fail-fast Y) se a fila do runner apertar. Impl: `docs/plans/ops62-ci-fail-fast-forgejo-impl.md` (a intenção fica como histórico do gate). +**Recently resolved (2026-08-18):** **OPS62** — fail-fast estrutural do CI: `ci-pr.yml` e `ci.yml` deixaram de ter jobs irmãos. Cada workflow agora tem **um único job `checks` sequencial** (ci-pr: guards de conteúdo → lint → format → typecheck → knip → cycles → unit → int → build → e2e, com skips do `ci-scope.mjs` agora como step com outputs; ci.yml: mesma mecânica full + deploy OPS53 intacto), então **step vermelho interrompe o job, o run falha e o slot do runner é liberado imediatamente** — sem API de cancel (o Forgejo 9.0.3 não tem), sem watchdog, sem upgrade do Forgejo no homeserver (decisão X1 do gate de execução substitui o watchdog+upgrade da intenção; o aceite "API de cancel/upgrade" e "mesmos jobs, mesma cascata" foi revisado pelo humano — o que o CI _verifica_ permanece idêntico, só o envelope muda). Diagnóstico preservado: steps têm logs próprios. O job id `checks` mantém o contexto `CI (PR) / checks` que o automerger/`waitForChecks` e a branch protection esperam (OPS61). Paralelismo do e2e recuperado por `PLAYWRIGHT_WORKERS=4` num único processo (substitui os 2 shards × 2 workers; mesma carga); **2 services Postgres por nome na rede do job** (`postgres-int`/`postgres-build`, **sem publish host** — o job único seguraria portas fixas pelo run inteiro e runs concorrentes colidiriam; o Forgejo 9 não expande expressões em `services.ports`, medido ao vivo no run 730 falhando em 1s com a string literal; o DNS da rede do job mata a colisão de raiz — os guards fail-closed `LOCAL_HOSTS`/`assertTestDatabase` admitem os nomes dos services, ainda rejeitando hosts remotos) preservam a topologia de hoje (e2e nunca herda resíduo de fixtures do int); 1 checkout + 1 `pnpm install` por run (antes ~10). Plumbing morto removido: `e2eShardConfig` e o output `e2e_shards` do classifier (sem consumidor). Guards de conteúdo movidos para o início do pipeline (falham rápido e são PR-scoped). `/simplify` (3 reviewers): P1 aplicados — `set -euo pipefail` no step `scope` (crash do classifier com outputs vazios silenciaria skips → false green) e services por nome (acima); P2 aplicados — pins de invariantes ancorados em chaves YAML (comentários não satisfazem `needs: [checks]`/`matrix:`/`fail-fast:`), pin do wiring crítico `id: scope` + `if: e2e_mode == 'full'` (perdê-lo pula a suíte em silêncio), comentário morto do sharding em `playwright.config.ts` e `docs/TESTING.md` corrigidos (ainda citavam `.github/workflows` e jobs paralelos); P3 — JSDoc do `ci-scope.mjs` e redação do impl plan; o P2 do `waitForChecks` (timeout 30 min vs job 50 min) fica como débito de CI anotado no impl. Docs: tabela CI do AGENT-OPS reescrita + linha morta do `styfle/cancel-workflow-action` (removida no OPS50) apagada. Sem migration/access/Consent/UI. Testes: invariantes novos em `ciSkipInvariants.unit.spec.ts` (um único job `checks`, sem `matrix:`/`fail-fast:`/`runs-on: host` no ci-pr, `PLAYWRIGHT_WORKERS: 4`, guards antes do lint, deploy gated em `needs: [checks]`, services por nome sem `ports:`); `localHosts.unit.spec.ts` com os nomes dos services; `testAffected.unit.spec.ts` sem o describe do shard config. **Validação ao vivo na própria entrega:** o ci-pr deste PR já é o envelope novo — o primeiro run (730) pegou a expressão de porta não suportada (achado → corrigido para DNS de service), o run verde prova a suíte inteira no job único; a medição do tempo de parede fica registrada no run. Custo aceito (X1): run verde ~21 min esperados (vs ~15–18 com jobs paralelos) e e2e vermelho é o caso mais lento de veredito; débito registrado no impl plan (matrix fail-fast Y) se a fila do runner apertar. Impl: `docs/plans/ops62-ci-fail-fast-forgejo-impl.md` (a intenção fica como histórico do gate). + **Recently resolved (2026-08-17):** **OPS51** — o banco de produção saiu do Neon para o Postgres do homeserver (`teqo_1313`, container `teqo-1313`), sem mudança de config: o `~/stack/teqo-1313.env` já apontava para o banco preparado (criado 16/08 com role própria + cadeia completa de 56 migrations + zero conteúdo — verificado), então restaurar = a produção "apareceu" no container. **Divergência deliberada da intenção (questão A/B validada como B):** rejeitado criar `teqo_prod` novo — o argumento "o banco do 1313 tem vida própria" não se sustenta (o site 1313 É a produção deste repo) e o OPS53 já trata `teqo_1313` como produção. Execução 100% no homeserver (PII nunca tocou a workstation): URL do Neon puxada do Vercel (`vercel env pull`, projeto `jorgesolla`, `DATABASE_URL_UNPOOLED` → `NEON_DATABASE_URL` em `~/stack/.env` chmod 600), `pg_dump -Fc` (Neon PG 17.10) → `/srv/hdd/backups/teqo-neon-pre-migracao/teqo-neon-full-20260817-204800.dump` (5,8 MB, sha256 `f12196f2…`), `pg_restore --no-owner --no-privileges` como a role `teqo_1313` (drop schema + grant, padrão do `db-pull`), verificado: contagens por tabela idênticas ao Neon (1919 contatos, 39 posts, 1487 assinaturas, 1484 subscrições, 35 campaign_users, 392 lideranças, 2 pledges, 435 municípios, 6 atividades; supporter segue 0 — blocker LGPD ainda de pé), sequences no topo (`last_value == max(id)`), objetos com dono `teqo_1313`, `payload migrate` no-op (56 = 56, serviço de maintenance `teqo-1313-migrate`), 3 credenciais WebAuthn + 34 sessions + 5 chaves Consent restauradas (RP ID não mudou — biometria segue valendo). Smoke sem deploy: `/`, artigo com conteúdo restaurado, `/campanha/login`, `/admin`, `POST /api/revalidate` (`posts` + `global_privacy-policy`), WebAuthn `login-options`, barreira 307 sem sessão — verdes; smoke de browser (login, biometria, convites) validado manualmente. **Rollback documentado:** Neon nunca foi escrito (só lido); voltar = re-apontar `DATABASE_URL` do `teqo-1313.env` para o Neon + `compose up -d`. **Débito registrado (→ OPS52):** as 40 rows de `media` têm URLs relativas (`/api/media/file/…`) sem arquivos em lugar nenhum — capas dão 500 no homeserver e **404 no próprio Vercel/produção atual** (estado pré-existente; migração manteve paridade). Repo: plano da intenção concluído, `AGENTS.md` e help de `db:pull`/`check-push-chain` sem a referência ao Neon; infra-solla `STATE.md` atualizado à parte. **Recently resolved (2026-08-17):** **OPS53** — o deploy do site 1313 virou etapa do CI: mergir em `main` com a suite verde == site atualizado em `jorgesolla1313.com.br`, sem passo manual. O job `deploy` em `.forgejo/workflows/ci.yml` roda no host do runner (workstation, `runs-on: host`) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH (`ssh homeserver "bash -s -- "`); o script remoto (bash puro, homeserver sem dependências novas) faz: guard de HEAD (`git ls-remote` de `main` == SHA do job, senão skip "stale"), `flock` de serialização, workspace `~/teqo-deploy` (clone do Forgejo local), `docker login localhost:5000` (REGISTRY_USER/PASSWORD de `~/stack/.env`), build dos stages `runner`+`migrator` no homeserver (`--network stack_default` para a geração estática alcançar o `postgres:5432` do compose; secrets de `~/stack/teqo-1313.env` via BuildKit, sem camadas/logs), push no registry local, swap dos tags de imagem no compose (com backup `docker-compose.yml.pre-`), **migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout**, `docker compose up -d`, espera do healthcheck e smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn `login-options` com body JSON, `POST /api/revalidate` com o secret real — rehearsal validado contra o site vivo). Falha pós-swap → rollback automático best-effort (restore do backup + `up -d` da imagem anterior) + job vermelho. **Divergência deliberada da intenção (fato novo):** a intenção assumia o runner no homeserver; verificado (`infra-solla/STATE.md` + ao vivo) que o runner é a `workstation-runner` na workstation e o stack de produção vive no homeserver — a abordagem A foi corrigida para "job `runs-on: host` na workstation + todo o deploy no homeserver via SSH", o que manteve **zero secrets novos no Forgejo** e zero mudança de infra no compose. Nenhum secret no repo; envs seguem chmod-600 no homeserver. Runbook (rollback manual, falhas conhecidas, decisão de segurança do host label): `docs/ops/teqo-1313-deploy.md` (novo). Guarda estrutural nova: `tests/unit/deployScript.unit.spec.ts` (bash -n + invariantes do pipeline: HEAD guard, BuildKit secrets, ordem migrate→rollout, smoke). AGENTS.md/AGENT-OPS.md atualizados. Primeiro deploy real = o merge deste próprio item (tracer bullet); infra-solla `STATE.md` anotado à parte. diff --git a/docs/changelog/2026-08-18-ops62.md b/docs/changelog/2026-08-18-ops62.md index ebc18914..b1ad4d67 100644 --- a/docs/changelog/2026-08-18-ops62.md +++ b/docs/changelog/2026-08-18-ops62.md @@ -1 +1 @@ -**Recently resolved (2026-08-18):** **OPS62** — fail-fast estrutural do CI: `ci-pr.yml` e `ci.yml` deixaram de ter jobs irmãos. Cada workflow agora tem **um único job `checks` sequencial** (ci-pr: guards de conteúdo → lint → format → typecheck → knip → cycles → unit → int → build → e2e, com skips do `ci-scope.mjs` agora como step com outputs; ci.yml: mesma mecânica full + deploy OPS53 intacto), então **step vermelho interrompe o job, o run falha e o slot do runner é liberado imediatamente** — sem API de cancel (o Forgejo 9.0.3 não tem), sem watchdog, sem upgrade do Forgejo no homeserver (decisão X1 do gate de execução substitui o watchdog+upgrade da intenção; o aceite "API de cancel/upgrade" e "mesmos jobs, mesma cascata" foi revisado pelo humano — o que o CI _verifica_ permanece idêntico, só o envelope muda). Diagnóstico preservado: steps têm logs próprios. O job id `checks` mantém o contexto `CI (PR) / checks` que o automerger/`waitForChecks` e a branch protection esperam (OPS61). Paralelismo do e2e recuperado por `PLAYWRIGHT_WORKERS=4` num único processo (substitui os 2 shards × 2 workers; mesma carga); **2 services Postgres isolados** (int e build+e2e) com **portas host derivadas do `run_id`** (`5433/5533 + run_id % 100` — o job único segura as duas portas pelo run inteiro; runs concorrentes colidiriam em portas fixas) preservam a topologia de hoje (e2e nunca herda resíduo de fixtures do int); 1 checkout + 1 `pnpm install` por run (antes ~10). Plumbing morto removido: `e2eShardConfig` e o output `e2e_shards` do classifier (sem consumidor). Guards de conteúdo movidos para o início do pipeline (falham rápido e são PR-scoped). `/simplify` (3 reviewers): P1 aplicados — `set -euo pipefail` no step `scope` (crash do classifier com outputs vazios silenciaria skips → false green) e portas por run (acima); P2 aplicados — pins de invariantes ancorados em chaves YAML (comentários não satisfazem `needs: [checks]`/`matrix:`/`fail-fast:`), pin do wiring crítico `id: scope` + `if: e2e_mode == 'full'` (perdê-lo pula a suíte em silêncio), comentário morto do sharding em `playwright.config.ts` e `docs/TESTING.md` corrigidos (ainda citavam `.github/workflows` e jobs paralelos); P3 — JSDoc do `ci-scope.mjs` e redação do impl plan; o P2 do `waitForChecks` (timeout 30 min vs job 50 min) fica como débito de CI anotado no impl. Docs: tabela CI do AGENT-OPS reescrita + linha morta do `styfle/cancel-workflow-action` (removida no OPS50) apagada. Sem migration/access/Consent/UI. Testes: invariantes novos em `ciSkipInvariants.unit.spec.ts` (um único job `checks`, sem `matrix:`/`fail-fast:`/`runs-on: host` no ci-pr, `PLAYWRIGHT_WORKERS: 4`, guards antes do lint, deploy gated em `needs: [checks]`); `testAffected.unit.spec.ts` sem o describe do shard config. **Validação ao vivo na própria entrega:** o ci-pr deste PR já é o envelope novo — o run verde prova a suíte inteira no job único; a medição do tempo de parede fica registrada no run. Custo aceito (X1): run verde ~21 min esperados (vs ~15–18 com jobs paralelos) e e2e vermelho é o caso mais lento de veredito; débito registrado no impl plan (matrix fail-fast Y) se a fila do runner apertar. Impl: `docs/plans/ops62-ci-fail-fast-forgejo-impl.md` (a intenção fica como histórico do gate). +**Recently resolved (2026-08-18):** **OPS62** — fail-fast estrutural do CI: `ci-pr.yml` e `ci.yml` deixaram de ter jobs irmãos. Cada workflow agora tem **um único job `checks` sequencial** (ci-pr: guards de conteúdo → lint → format → typecheck → knip → cycles → unit → int → build → e2e, com skips do `ci-scope.mjs` agora como step com outputs; ci.yml: mesma mecânica full + deploy OPS53 intacto), então **step vermelho interrompe o job, o run falha e o slot do runner é liberado imediatamente** — sem API de cancel (o Forgejo 9.0.3 não tem), sem watchdog, sem upgrade do Forgejo no homeserver (decisão X1 do gate de execução substitui o watchdog+upgrade da intenção; o aceite "API de cancel/upgrade" e "mesmos jobs, mesma cascata" foi revisado pelo humano — o que o CI _verifica_ permanece idêntico, só o envelope muda). Diagnóstico preservado: steps têm logs próprios. O job id `checks` mantém o contexto `CI (PR) / checks` que o automerger/`waitForChecks` e a branch protection esperam (OPS61). Paralelismo do e2e recuperado por `PLAYWRIGHT_WORKERS=4` num único processo (substitui os 2 shards × 2 workers; mesma carga); **2 services Postgres por nome na rede do job** (`postgres-int`/`postgres-build`, **sem publish host** — o job único seguraria portas fixas pelo run inteiro e runs concorrentes colidiriam; o Forgejo 9 não expande expressões em `services.ports`, medido ao vivo no run 730 falhando em 1s com a string literal; o DNS da rede do job mata a colisão de raiz — os guards fail-closed `LOCAL_HOSTS`/`assertTestDatabase` admitem os nomes dos services, ainda rejeitando hosts remotos) preservam a topologia de hoje (e2e nunca herda resíduo de fixtures do int); 1 checkout + 1 `pnpm install` por run (antes ~10). Plumbing morto removido: `e2eShardConfig` e o output `e2e_shards` do classifier (sem consumidor). Guards de conteúdo movidos para o início do pipeline (falham rápido e são PR-scoped). `/simplify` (3 reviewers): P1 aplicados — `set -euo pipefail` no step `scope` (crash do classifier com outputs vazios silenciaria skips → false green) e services por nome (acima); P2 aplicados — pins de invariantes ancorados em chaves YAML (comentários não satisfazem `needs: [checks]`/`matrix:`/`fail-fast:`), pin do wiring crítico `id: scope` + `if: e2e_mode == 'full'` (perdê-lo pula a suíte em silêncio), comentário morto do sharding em `playwright.config.ts` e `docs/TESTING.md` corrigidos (ainda citavam `.github/workflows` e jobs paralelos); P3 — JSDoc do `ci-scope.mjs` e redação do impl plan; o P2 do `waitForChecks` (timeout 30 min vs job 50 min) fica como débito de CI anotado no impl. Docs: tabela CI do AGENT-OPS reescrita + linha morta do `styfle/cancel-workflow-action` (removida no OPS50) apagada. Sem migration/access/Consent/UI. Testes: invariantes novos em `ciSkipInvariants.unit.spec.ts` (um único job `checks`, sem `matrix:`/`fail-fast:`/`runs-on: host` no ci-pr, `PLAYWRIGHT_WORKERS: 4`, guards antes do lint, deploy gated em `needs: [checks]`, services por nome sem `ports:`); `localHosts.unit.spec.ts` com os nomes dos services; `testAffected.unit.spec.ts` sem o describe do shard config. **Validação ao vivo na própria entrega:** o ci-pr deste PR já é o envelope novo — o primeiro run (730) pegou a expressão de porta não suportada (achado → corrigido para DNS de service), o run verde prova a suíte inteira no job único; a medição do tempo de parede fica registrada no run. Custo aceito (X1): run verde ~21 min esperados (vs ~15–18 com jobs paralelos) e e2e vermelho é o caso mais lento de veredito; débito registrado no impl plan (matrix fail-fast Y) se a fila do runner apertar. Impl: `docs/plans/ops62-ci-fail-fast-forgejo-impl.md` (a intenção fica como histórico do gate). diff --git a/docs/plans/ops62-ci-fail-fast-forgejo-impl.md b/docs/plans/ops62-ci-fail-fast-forgejo-impl.md index 610affb5..905a7afa 100644 --- a/docs/plans/ops62-ci-fail-fast-forgejo-impl.md +++ b/docs/plans/ops62-ci-fail-fast-forgejo-impl.md @@ -40,8 +40,9 @@ flowchart TB ### Componentes / mudanças -- **`.forgejo/workflows/ci-pr.yml`** (reescrito — X1): 1 job `checks` (nome default → contexto `CI (PR) / checks` preservado; **o nome do job é o contrato** do waitForChecks OPS61-2b e da branch protection). Steps em série, cada um com `if:` de skip do classifier: `scope` (`id: scope`; outputs `code_mode/build_mode/test_mode/e2e_mode/e2e_specs` via `$GITHUB_OUTPUT` — mesmo comando do job atual, com `set -euo pipefail`), guards de conteúdo (changelog append-only, aggregate, conflict markers, plans-only — movidos para o começo: falham rápido e são sobre o PR), `check-test-locations`, `lint`, `format:check`, `typecheck`/`knip`/`cycles` (`if: steps.scope.outputs.code_mode != 'none'`), `unit` (full/changed), `migrate`+`seed:minimal` (DB int), `int` (full/changed), `migrate`+`seed:minimal` (DB build/e2e), `build` (if `build_mode != 'none'`), `build .next-e2e` + `e2e` (if `e2e_mode != 'none'`; full = `pnpm test:e2e` sem shard com `PLAYWRIGHT_WORKERS=4`; selected = specs do output). **2 services Postgres** com **portas host derivadas do `run_id`** (`5433 + run_id % 100` int, `5533 + run_id % 100` build/e2e — o job único segura as duas portas pelo run inteiro e runs concorrentes no runner compartilhado colidiriam em portas fixas; resíduo ~1% é ruidoso e resolvido no push seguinte; mesma topologia gateway OPS50) preservando o isolamento de hoje (o e2e nunca herda a sujeira dos testes int; o build de prod e o de e2e compartilham o service build/e2e). `timeout-minutes: 50` (soma esperada ~21 min + margem; o run nunca segura o slot além disso). Sem `needs`/`if: always()`/matrix/jq de rollup (não há irmãos). Comentário de cabeçalho reescrito: OPS62 substitui o parágrafo "Forgejo has no cancel-run plumbing; job timeouts cover that role" — o fail-fast é estrutural (1 job), sem API de cancel. **Trade-off assumido:** `migrate`+`seed:minimal` do int rodam também em PR docs-only (mode none) — intencional (a cadeia stale é pega em todo PR), custo ~1–2 min por PR de docs. -- **`.forgejo/workflows/ci.yml`** (reescrito — X1): mesma estrutura no `main`; `static` (já era job único sequencial) se funde ao job `checks` com int/build/e2e; `deploy` permanece `needs: [checks]` (inalterado — OPS53). `timeout-minutes: 50`. +- **`.forgejo/workflows/ci-pr.yml`** (reescrito — X1): 1 job `checks` (nome default → contexto `CI (PR) / checks` preservado; **o nome do job é o contrato** do waitForChecks OPS61-2b e da branch protection). Steps em série, cada um com `if:` de skip do classifier: `scope` (`id: scope`; outputs `code_mode/build_mode/test_mode/e2e_mode/e2e_specs` via `$GITHUB_OUTPUT` — mesmo comando do job atual, com `set -euo pipefail`), guards de conteúdo (changelog append-only, aggregate, conflict markers, plans-only — movidos para o começo: falham rápido e são sobre o PR), `check-test-locations`, `lint`, `format:check`, `typecheck`/`knip`/`cycles` (`if: steps.scope.outputs.code_mode != 'none'`), `unit` (full/changed), `migrate`+`seed:minimal` (DB int), `int` (full/changed), `migrate`+`seed:minimal` (DB build/e2e), `build` (if `build_mode != 'none'`), `build .next-e2e` + `e2e` (if `e2e_mode != 'none'`; full = `pnpm test:e2e` sem shard com `PLAYWRIGHT_WORKERS=4`; selected = specs do output). **2 services Postgres alcançados por NOME na rede do job** (`postgres-int` / `postgres-build` na 5432, **sem publish host** — o OPS50 publicava portas no gateway; o X1 remove o publish: o job único seguraria as portas fixas pelo run inteiro e runs concorrentes colidiriam, e o Forgejo 9 não expande expressões em `services.ports` (medido ao vivo: run 730 falhou em 1s com a string literal da expressão) — o DNS da rede do job é a solução canônica e mata a colisão de raiz). Isolamento preservado (o e2e nunca herda a sujeira dos testes int; o build de prod e o de e2e compartilham o service build/e2e). `timeout-minutes: 50` (soma esperada ~21 min + margem; o run nunca segura o slot além disso). Sem `needs`/`if: always()`/matrix/jq de rollup (não há irmãos). Comentário de cabeçalho reescrito: OPS62 substitui o parágrafo "Forgejo has no cancel-run plumbing; job timeouts cover that role" — o fail-fast é estrutural (1 job), sem API de cancel. **Trade-off assumido:** `migrate`+`seed:minimal` do int rodam também em PR docs-only (mode none) — intencional (a cadeia stale é pega em todo PR), custo ~1–2 min por PR de docs. +- **`.forgejo/workflows/ci.yml`** (reescrito — X1): mesma estrutura no `main`; `static` (já era job único sequencial) se funde ao job `checks` com int/build/e2e; `deploy` permanece `needs: [checks]` (inalterado — OPS53). `timeout-minutes: 50`. Services por nome, sem publish (mesma mecânica do ci-pr). +- **`scripts/lib/cli.mjs` + `tests/helpers/assertTestDatabase.ts`** (editar — guards fail-closed): `LOCAL_HOSTS`/allowlist do teste ganham `postgres-int` e `postgres-build` (nomes dos services do CI na rede do job — hosts internos do runner, inalcançáveis de fora; o contrato continua rejeitando qualquer host remoto real; o gateway RFC1918 do OPS50 permanece por compatibilidade). Pin em `tests/unit/localHosts.unit.spec.ts`. - **`tests/unit/ciSkipInvariants.unit.spec.ts`** (editar — pin da arquitetura): invariantes novos — os dois workflows têm **exatamente um job** cujo id é `checks`; os steps-chave existem na ordem (lint antes de e2e; guards antes de lint); o ci-pr não contém `runs-on: host` (pin OPS53), nem `matrix:`, nem `fail-fast:`; `PLAYWRIGHT_WORKERS: 4` presente; `.next-e2e` continua pinado (invariante existente); o ci.yml mantém o job `deploy` com `needs: [checks]`. - **`docs/AGENT-OPS.md`** (editar): tabela de CI — ci-pr/ci agora 1 job `checks` sequencial com fail-fast estrutural; remover menção a "fase 1→2 em cascata" se houver; nota "Dono do PR, dono do CI" intacta (o próprio PR define seu CI). - **`docs/changelog/2026-08-18-ops62.md`** (novo) + `pnpm changelog:build`. diff --git a/scripts/lib/cli.mjs b/scripts/lib/cli.mjs index 62abfa7d..6621ead4 100644 --- a/scripts/lib/cli.mjs +++ b/scripts/lib/cli.mjs @@ -29,8 +29,21 @@ export const loadCliEnv = () => { loadEnv({ path: '.env' }) } -/** Local database hosts — including the docker-compose service name. THE one list. */ -export const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) +/** + * Local database hosts — docker-compose service name, the job-network service + * names of the self-hosted Forgejo runner (OPS62 X1: services are reached by + * name, no host publish), and the bridge gateway (OPS50: legacy per-port + * publish kept in the allowlist). THE one list. + */ +export const LOCAL_HOSTS = new Set([ + 'localhost', + '127.0.0.1', + '0.0.0.0', + '::1', + 'postgres', + 'postgres-int', + 'postgres-build', +]) /** * Gateway of the default route, read from /proc/net/route (Linux). The diff --git a/tests/helpers/assertTestDatabase.ts b/tests/helpers/assertTestDatabase.ts index 38b2bd3d..ab94abaa 100644 --- a/tests/helpers/assertTestDatabase.ts +++ b/tests/helpers/assertTestDatabase.ts @@ -12,9 +12,10 @@ /** `teqo_test`, `teqo_wt15_test`, … — the _test suffix is the hard contract. */ const TEST_DATABASE_NAME = /^teqo(_[a-z0-9]+)?_test$/ -// Self-hosted Forgejo runner: the job's postgres service is published on the -// bridge gateway IP (RFC1918), not localhost. Same source as the CLI guard so -// both guards admit exactly the same hosts (OPS50). +// 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). import { defaultGatewayHost } from '../../scripts/lib/cli.mjs' export function assertTestDatabase(databaseUrl: string | undefined): void { @@ -33,7 +34,14 @@ export function assertTestDatabase(databaseUrl: string | undefined): void { } const databaseName = decodeURIComponent(parsedUrl.pathname.replace(/^\//, '')) - const localHosts = new Set(['localhost', '127.0.0.1', '::1', '[::1]']) + const localHosts = new Set([ + 'localhost', + '127.0.0.1', + '::1', + '[::1]', + 'postgres-int', + 'postgres-build', + ]) const gateway = defaultGatewayHost() if (gateway) localHosts.add(gateway) diff --git a/tests/unit/ciSkipInvariants.unit.spec.ts b/tests/unit/ciSkipInvariants.unit.spec.ts index 3a0fc1b3..8982958c 100644 --- a/tests/unit/ciSkipInvariants.unit.spec.ts +++ b/tests/unit/ciSkipInvariants.unit.spec.ts @@ -112,6 +112,19 @@ describe('ciSkipInvariants', () => { expect(main).toMatch(/^ needs: \[checks\]\n/m) }) + it('OPS62 X1: services are reached by name on the job network (no host ports)', () => { + // Forgejo 9 does not expand expressions in `services.ports` (measured + // live: run 730 failed 1s with the literal port string) and a fixed host + // port would collide between concurrent runs — the single long-lived job + // holds its services for the whole run. Pinned: DNS by name, no ports. + for (const file of ['.forgejo/workflows/ci.yml', '.forgejo/workflows/ci-pr.yml']) { + const source = readFileSync(join(repoRoot, file), 'utf8') + expect(source, file).toContain('@postgres-int:5432/teqo_test') + expect(source, file).toContain('@postgres-build:5432/teqo_test') + expect(source, file).not.toMatch(/^ {6}ports:\n/m) + } + }) + it('keeps local e2e build artifacts outside the development dist directory', () => { for (const file of [ '.forgejo/workflows/ci.yml', diff --git a/tests/unit/localHosts.unit.spec.ts b/tests/unit/localHosts.unit.spec.ts index a15331d2..aaad5cc0 100644 --- a/tests/unit/localHosts.unit.spec.ts +++ b/tests/unit/localHosts.unit.spec.ts @@ -4,18 +4,23 @@ import { describe, expect, it } from 'vitest' import { defaultGatewayHost, LOCAL_HOSTS } from '../../scripts/lib/cli.mjs' -describe('LOCAL_HOSTS (OPS50 — self-hosted runner reaches services via the bridge gateway)', () => { +describe('LOCAL_HOSTS (OPS50/OPS62 — self-hosted runner services by name, legacy gateway)', () => { it('still contains the canonical static hosts', () => { for (const host of ['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) { expect(LOCAL_HOSTS.has(host)).toBe(true) } }) + it('admits the job-network service names (OPS62 X1: no host publish)', () => { + expect(LOCAL_HOSTS.has('postgres-int')).toBe(true) + expect(LOCAL_HOSTS.has('postgres-build')).toBe(true) + }) + it('admits the machine default gateway when it is RFC1918 (Linux CI bridge)', () => { const gateway = defaultGatewayHost() if (gateway) { - // The Forgejo runner publishes the job's postgres service on the bridge - // gateway IP; assertLocalDatabase must accept it so seeds run in CI. + // The Forgejo runner published the job's postgres service on the bridge + // gateway IP before OPS62; assertLocalDatabase must still accept it. expect(LOCAL_HOSTS.has(gateway)).toBe(true) } }) @@ -24,5 +29,6 @@ describe('LOCAL_HOSTS (OPS50 — self-hosted runner reaches services via the bri expect(LOCAL_HOSTS.has('8.8.8.8')).toBe(false) expect(LOCAL_HOSTS.has('127.0.0.2')).toBe(false) expect(LOCAL_HOSTS.has('203.0.113.1')).toBe(false) + expect(LOCAL_HOSTS.has('postgres-prod')).toBe(false) }) })