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).
12 KiB
Impl: OPS62 — CI: fail-fast estrutural por job único sequencial (X1)
Status: aprovado Atualizado em: 2026-08-18 Issue: #59 Intenção: docs/plans/ops62-ci-fail-fast-forgejo.md Appetite restante: ~1 dia eng (corte: nada de watchdog/API/upgrade — decidido no gate de execução)
Leitura da intenção
- Outcome: um run de PR (ou de
main) com qualquer verificação vermelha falha o run imediatamente — nada continua rodando depois da primeira falha; o runner é liberado cedo;checksvermelho sem esperar o fim dos demais; logs do que já rodou preservados; a suíte verificada não muda (mesmas ferramentas e conteúdo: lint, format, typecheck, knip, cycles, unit, int, build, e2e, guards); sem bypass novo além do--no-verifydocumentado. - O que NÃO negociar: anti-goals — não perder diagnóstico (steps preservam logs próprios), não mudar o que o CI verifica (só o envelope de jobs muda),
checkscomo gate único (contextoCI (PR) / checkspreservado para waitForChecks/branch protection); aceitar a perda dos logs do segundo shard de e2e (o e2e perde o paralelismo de jobs — 4 workers num único processo; o gate aceitou perder o shard irmão, e aqui o sharding em si é compensado por workers). - Divergência do aceite da intenção (aprovada pelo humano no gate de execução, 2026-08-18): o aceite "Forgejo do homeserver roda versão com a API de cancel" e "mesmos jobs, mesma cascata" são substituídos — a arquitetura escolhida (X1: 1 job sequencial) torna a API de cancel e o upgrade desnecessários (fail-fast é estrutural) e muda o agrupamento de jobs (o que é verificado permanece idêntico). O plano de intenção fica como histórico; este impl plan é a forma aprovada.
Abordagem recomendada
flowchart TB
subgraph job[Um único job `checks` por workflow<br/>1 container runner · 1 checkout · 1 pnpm install · 1 slot]
S1[scope classifier] --> S2[guards de conteúdo]
S2 --> S3[check-test-locations] --> L[lint] --> F[format] --> T[typecheck]
T --> K[knip] --> C[cycles] --> U[unit]
U --> M1[migrate + seed 5433] --> I[int]
I --> M2[migrate + seed 5434] --> B1[build] --> B2[build .next-e2e]
B2 --> E[e2e · PLAYWRIGHT_WORKERS=4]
end
L -->|"falha em qualquer step<br/>(if: steps.scope.outputs …)"| FIM
F -->|"steps seguintes não rodam"| FIM[run falha imediato<br/>slot liberado · checks vermelho<br/>logs dos steps rodados preservados]
Opções consideradas (gate de execução): X1 | Y | Z
Recomendação (escolhida pelo humano): X1 — 1 job único por workflow, steps sequenciais, fail-fast estrutural: falhou um step → os seguintes nem começam → job falha → run falha → slot liberado. Zero dependência de API de cancel (a rota nem existe no Forgejo 9.0.3), zero watchdog (não há slot extra, token, upgrade, nem risco de infra no homeserver — o Forgejo não é atualizado). O paralelismo do e2e é recuperado por PLAYWRIGHT_WORKERS=4 (o Playwright roda workers dentro de um processo — mesma carga de 2 shards × 2 workers de hoje).
Rejeitadas:
- Y — fase 1 sequencial + fase 2 em matrix
fail-fast: true: preserva verde ≈ atual e cancela irmãos nativamente, mas depende de o Forgejo 9.0.3 cancelar de fato os irmãos de matrix (comportamento de servidor a verificar ao vivo; se falhasse, virava "tudo roda até o fim" na fase 2), mantém 2 slots em vez de 1, e o YAML de matrix pormodeé mais complexo de manter que steps em série. Rejeitada por simplicidade máxima pedida. - Z — watchdog + upgrade do Forgejo 16: o risco de infra (upgrade de produção no homeserver, compatibilidade runner × v16) deixou de se justificar quando o fail-fast estrutural cobre o aceite com menos peças. Rejeitada.
- Watchdog em
runs-on: host: ci-pr jamais roda host (pin OPS53). Rejeitada. - Paralelizar steps com processos
&/wait: logs interleaved + fail-fast manual = mini-watchdog em bash. Rejeitada.
Componentes / mudanças
.forgejo/workflows/ci-pr.yml(reescrito — X1): 1 jobchecks(nome default → contextoCI (PR) / checkspreservado; o nome do job é o contrato do waitForChecks OPS61-2b e da branch protection). Steps em série, cada um comif:de skip do classifier:scope(id: scope; outputscode_mode/build_mode/test_mode/e2e_mode/e2e_specsvia$GITHUB_OUTPUT— mesmo comando do job atual, comset -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(ifbuild_mode != 'none'),build .next-e2e+e2e(ife2e_mode != 'none'; full =pnpm test:e2esem shard comPLAYWRIGHT_WORKERS=4; selected = specs do output). 2 services Postgres alcançados por NOME na rede do job (postgres-int/postgres-buildna 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 emservices.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). Semneeds/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:minimaldo 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 nomain;static(já era job único sequencial) se funde ao jobcheckscom int/build/e2e;deploypermaneceneeds: [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 ganhampostgres-intepostgres-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 emtests/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émruns-on: host(pin OPS53), nemmatrix:, nemfail-fast:;PLAYWRIGHT_WORKERS: 4presente;.next-e2econtinua pinado (invariante existente); o ci.yml mantém o jobdeploycomneeds: [checks].docs/AGENT-OPS.md(editar): tabela de CI — ci-pr/ci agora 1 jobcheckssequencial 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.- Migration / Access / Consent / UI: N/A (Impeccable A).
- Fora do repo: nada — o upgrade do Forgejo é cancelado (decisão X1); o homeserver não é tocado.
Dados → forma (se aplicável)
N/A — chore de DX/processo.
Fases verificáveis
- Workflows X1 — reescrever
ci-pr.ymleci.yml(job único, 2 services, steps condicionais);bash/YAML válido (sem parser YAML no repo — validar por leitura + os invariantes). Gates parciais:pnpm lint,format:check,typecheck,knip,check:cycles. - Invariantes —
ciSkipInvariants.unit.spec.ts(pin da arquitetura: 1 jobchecks, ordem dos steps, sem host/matrix/fail-fast no ci-pr, deploy no ci.yml);pnpm test:unit(spec novo). - Docs + fechamento — AGENT-OPS, changelog +
changelog:build; gates completos (pnpm testint incluso,pnpm build); push viapnpm push. - Validação ao vivo (no próprio PR do OPS62) — o ci-pr deste PR já é o X1: (a) run verde prova que a suíte inteira roda no envelope novo; (b) o CI do PR com um step quebrado (ex.: antes do merge, um push deliberado com
lintquebrado) prova o fail-fast: run falha no primeiro vermelho, steps seguintes nem começam, logs preservados — verificação documentada no fechamento. Sem passo de infra pós-merge.
Rabbit holes / Não escopo (engenharia)
- Watchdog, API de cancel, upgrade do Forgejo, token PAT: fora — X1 não precisa de nenhum (a intenção fica como histórico).
- Mudar o que o CI verifica: fora — mesmas ferramentas, mesmo conteúdo, mesma ordem de suíte; só o envelope muda.
- Rerun granular de steps (re-tentar só o que falhou): o Forgejo não tem rerun por API (session-only); re-push continua sendo o caminho — como hoje.
- Voltar a shards de e2e em jobs separados: se o tempo de e2e full com 4 workers incomodar (medida no run deste PR), o caminho é Y (matrix fail-fast), não o watchdog — registrar como débito se medir > ~10 min.
- Timeout do
waitForChecks(30 min) × jobchecks(50 min) — débito de CI (revisão /simplify P2): um run verde pode passar dos 30 min com fila do runner/retries de e2e e o automerger lançar "Timeout esperando checks" num PR verde. Gatilho para corrigir: primeiro timeout observado. Alternativa curta (1 linha) registrada: alinhartimeoutMsdowaitForChecks/autoMergenoforgejo-api.mjsao timeout do job (50–55 min).
Riscos e mitigação
- Tempo de parede do run verde aumenta (~15–18 → ~21 min esperados: perde a paralelização da fase 1 e os 2 shards de e2e; ganha 1 checkout/install e 1 migrate/seed). Mitigação: medido no run deste PR; e2e com 4 workers compensa os shards; o aceite X1 do humano cobre o custo.
- E2E vermelho é o caso mais lento (veredito só após todos os steps anteriores). Mitigação: guards/lint/format/typecheck falham cedo (os casos comuns); e2e é a falha menos frequente e o report mostra o spec que falhou. Débito registrado (Y) se a frequência exigir.
- Step
scopecom outputs e steps condicionais — trocaneeds.scope.outputsporsteps.scope.outputs: mesma semântica, semântica de skip idêntica (none → steps skipped, não falham). Validado por um PR de docs (mode none) no próprio repo. - 1 service compartilhado int/e2e — rejeitado no plano (2 services 5433/5434 preservam o isolamento; risco zero de fixture colidir).
checkscomo job único muda o status check de "rollup que espera needs" para "o próprio run" — contexto e nome preservados (checks), então waitForChecks (OPS61-2b) e branch protection continuam vendo o contexto certo; quando o job falha, o status do sha éfailure— veredito mais direto ainda.- Run de main (
ci.yml): com deployneeds: checks, qualquer falha agora impede o deploy imediatamente (hoje também, mas só no fim dos irmãos) — mesma garantia, veredito mais cedo.
Aceite de engenharia
- Aceite de produto coberto (forma X1 aprovada pelo humano): qualquer verificação vermelha falha o run no ato; runner liberado cedo;
checksvermelho sem esperar o fim dos demais; logs preservados (steps com logs próprios); suíte verificada idêntica; sem bypass novo. - Invariantes AGENTS/engineering-standards: sem migration/access/Consent/UI; pin da arquitetura em spec (1 job
checks, sem host no ci-pr, sem matrix/fail-fast); identifiers em inglês; copy pt-BR só em labels (inalterado). - Testes previstos: unit dos invariantes de workflow (não há write path de app).