mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
fix(OPS62): services por nome na rede do job — Forgejo 9 não expande expressões em ports
Achado ao vivo (run 730): a expressão ${{ 5433 + (github.run_id % 100) }} no
ports do service não é avaliada pelo Forgejo 9 — o literal ia ao runner e o
job falhava em 1s. E portas fixas colidiriam entre runs concorrentes (o job
único segura os services pelo run inteiro). Correção: services sem publish
host, alcançados por nome na rede do job (postgres-int/postgres-build:5432)
— solução canônica do Actions, mata a colisão de raiz. Guards fail-closed
(LOCAL_HOSTS + assertTestDatabase) admitem os nomes dos services (hosts
internos do runner; hosts remotos continuam rejeitados); pins novos em
localHosts.unit.spec.ts e ciSkipInvariants (sem ports:, URLs por nome).
This commit is contained in:
parent
12a4fe1005
commit
ee31aeee4f
9 changed files with 88 additions and 62 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,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 -- <sha>"`); 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-<sha>`), **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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue