O sintoma primário da Issue (deadlock do waitForChecks no próprio pending) já tinha sido entregue pelo OPS61. O secundário — o rollup `checks` reportava SUCCESS com um shard de e2e vermelho porque o Forgejo 9.0.3 reporta o resultado do ÚLTIMO shard como o parent de matrix no `needs` (runs 604/607/610; o `needs.<job>.results` do GitHub não existe neste runner) — foi eliminado estruturalmente pelo OPS62 (mergeado durante a execução): ci-pr.yml e ci.yml viraram jobs únicos sequenciais, sem matrix/shard, e o status do job `checks` já é o veredito honesto da cascata. O que este item entrega: (a) teste-pino no waitForChecks (cenário run 607: rollup verde + shard vermelho → o CLI LANÇA "Checks falharam" — nunca confia no rollup sozinho; invariante de defesa válido e que pegaria qualquer matrix que volte); (b) docs honestas (agent-pr-workflow.mdc, AGENT-OPS.md) com a narrativa do parent de matrix e da eliminação estrutural pelo OPS62. O script ci-rollup-check.mjs e os edits de workflow planejados foram descartados no gate (redundantes pós-OPS62). Unit 2392 verdes; gates locais ok.
14 KiB
Impl: OPS64 — forgejo-pr-automerge: waitForChecks conta o próprio status pendente (deadlock → timeout, merge nunca acontece) + rollup checks sem agregação de shards
Status: em execução (entregue — ver desfecho no final) Atualizado em: 2026-08-18 Issue: #62 Intenção: body da Issue (sem plano de intenção separado — body é a spec) Appetite restante: herdado (~1 dia de ops)
Leitura da intenção
- Outcome: (primário) o safety net
ready-automergedeixa de se auto-bloquear no próprio statuspendinge mergea PRs com CI verde; (secundário) o rollupchecksdoci-pr.yml(e o doci.yml, que gateia o deploy) deixa de reportar SUCCESS com um shard de e2e vermelho — o gate do servidor (branch protection) e o UI param de mentir. - O que NÃO negociar: merge só com CI verde de verdade (a classe OPS52 — merge manual/automático com shard vermelho — não pode voltar);
waitForCheckscontinua ignorando o próprio contexto; nada de fork rodando código com o PAT do repo (guarda do OPS61); sem edição de Issuesin-progressde terceiros; nada de prod DB. - O que reavaliar: a intenção lista três correções possíveis para o primário (filtrar próprio context /
merge_when_checks_succeednativo / timeout > 40 min) — a evidência mostra que o primário já foi entregue pelo OPS61 (issue criada antes do OPS61 subir); o trabalho real do OPS64 é o secundário (agregação de shards no rollup), que está vivo e medido.
Achados da exploração (evidência ao vivo 2026-08-18)
- Primário (deadlock do próprio pending) — JÁ ENTREGUE pelo OPS61 (
8b989306, emmain):waitForChecksfiltra contextosPR Ready + auto-merge(verdicts), gateia no rollupCI (PR) / checkspresente + success, e o merge POST usaFORGEJO_API_TOKEN(409 do pre-receive). Provado vivo: runs 710 (PR #67) e 712 (PR #70) doready-automergeterminaramsuccesse os PRs foram mergeados no momento do fim do run (21:32:24/21:35:24 vs fim 21:32:27/21:35:31) — o CLI mergeou. O timeout já subiu para 45 min (CLI) / 55 min (workflow). A opção "timeout > 40 min" da intenção também já está entregue. - Secundário (rollup
checksmente) — VIVO e medido em 5 runs: o resultado do job-pai de matrix no contextoneedsdo Forgejo 9.0.3 (gitea-1.22.0) é o resultado do último shard a reportar, não um AND sobre os shards:- runs 604/607/610 (PR #55/#56/#57, ci-pr pré-OPS61 — mesmo jq de hoje): e2e (1)=failure, e2e (2)=success →
checks=SUCCESS (shard vermelho mascarado); - run 716 (PR #71, pós-OPS61): e2e (1)=success, e2e (2)=failure →
checks=failure (o vermelho era o último — pegou por acaso); - run 638: e2e (1)=failure, e2e (2)=success →
checks=failure só porque int e build também falharam (o e2e vermelho continuou mascarado). Ou seja: o gateCI (PR) / checks*do servidor e o rollup jq (all(.value.result == "success" or "skipped")) passam com um shard vermelho quando o último shard é verde.
- runs 604/607/610 (PR #55/#56/#57, ci-pr pré-OPS61 — mesmo jq de hoje): e2e (1)=failure, e2e (2)=success →
- Por que o CLI ainda é seguro hoje:
waitForChecksvarre todos os statuses individuais (failedincluiCI (PR) / e2e (1)= failure → throw). Foi por isso que no run 607 "só o waitForChecks pegou o vermelho". O buraco é o gate do servidor (branch protection satisfeita por rollup verde) + o UI — e odeploydoci.yml(mesmo jq) rodaria com shard vermelho em main. - Não existe
needs.<matrix>.resultsno runner do Forgejo 9.0.3: oNeedsdo act (gitea/actpkg/exprparser/interpreter.go) tem sóOutputseResult— o contrato.resultsdo GitHub não existe aqui. A correção do rollup não pode vir do contextoneeds. merge_when_checks_succeedexiste neste servidor (swagger + fonte gitea 1.22:ScheduleAutoMerge→IsPullCommitStatusPass), mas o OPS50/61 escolheu poll+merge e ele funciona (achado 1). Além disso o auto-merge nativo gateia nos contextos exigidos da branch protection — o mesmo rollup mentiroso — então não corrigiria o secundário; e omergeabledo servidor também usa o mesmoMergeRequiredContextsCommitStatus. Decisão: manter poll+merge; corrigir o rollup.- Shards já postam status individuais: cada shard do e2e posta
CI (PR) / e2e (N) (pull_request)(eCI / e2e (N) (push)no main) — o dado confiável é o status do commit, não o parent doneeds. É a mesma fonte que owaitForChecksusa (listCommitStatuses). - Risco RCE ao colocar PAT no ci-pr.yml: o
ci-pr.ymlroda em PRs de fork (repo público, sem guard no workflow). InjetarFORGEJO_API_TOKENno jobcheckssem o guard same-repo seria código do fork rodando com o PAT (a mesma classe que o OPS61 guardou nos pós-merge). Qualquer uso de PAT noci-pr.ymlexigeif: head.repo.full_name == github.repositoryno job.
Abordagem recomendada
flowchart LR
A[rollup checks: jq sobre needs parent de matrix MENTE] --> B[novo script ci-rollup-check.mjs lê statuses do sha via API]
C[waitForChecks já varre statuses individuais - seguro] --> D[nada a mudar no CLI]
B --> E[ci-pr: fork guard + FORGEJO_API_TOKEN + script]
B --> F[ci.yml: FORGEJO_API_TOKEN + script - gate do deploy honesto]
G[testes unit: veredito do script + waitForChecks com rollup verde e shard vermelho] --> H[PR Closes #62]
Opções consideradas:
- A — rollup lê os statuses do commit via API (
listCommitStatuses), com o mesmo prefixo do workflow (CI (PR) /,CI /) e excluindo o próprio contexto (…/ checks): veredito honesto por construção, mesma fonte dowaitForChecks, funciona para oci-pr.ymle oci.yml(gate do deploy). - B —
needs.<matrix>.results(contrato GitHub): inexistente no act do Forgejo 9.0.3 (fonte, achado 4). - C —
merge_when_checks_succeednativo (opção da intenção): existe no servidor, mas gateia nos contextos exigidos (rollup mentiroso) — não corrige o secundário; poll+merge atual já funciona (achado 1). - D — quebrar a matrix em jobs explícitos (e2e-1/e2e-2): mexe no contrato OPS34 (
e2eShardConfigpinado por unit testshard: [1, 2]), refatora o classificador de escopo; não é o dono do problema (o dono é o veredito do rollup). - E — timeout > 40 min (opção da intenção): já entregue (45/55); não corrige a mentira do rollup.
- F — deixar como está (rollup pode mentir, "o CLI pega"): deixa o gate do servidor e o
deployvulneráveis à classe OPS52.
Recomendação: A — porque é o único caminho que (a) usa a mesma fonte que o CLI já confia (statuses do commit — não o parent de matrix), (b) corrige os dois rollups (PR e main/deploy), (c) não mexe no contrato OPS34, (d) é testável como função pura. O PAT no ci-pr.yml entra com o fork guard no job checks (achado 7).
Rejeitadas: B (não existe no runner — falha silenciosa ou dependência de upgrade); C (não corrige o secundário — o auto-merge nativo confiaria no rollup mentiroso; e o poll+merge provado continua); D (refatora contrato OPS34 sem necessidade); E (já entregue, não é fix); F (classe OPS52 no gate do servidor + deploy vermelho em main).
Componentes / mudanças
scripts/lib/ci-rollup-check.mjs(novo, puro, zero-dep):rollupVerdict(statuses, { prefix, ownPrefix })→{ ok, failed: string[], pending: string[] }— falha (exit 1) se qualquer contexto com o prefixo do workflow (excluindo o próprio rollup) forfailure/error; falha também se houverpending(cascata não assentada — fail-closed); falha se a lista vier vazia (API indisponível/estranho — nunca "verde por falta de dado"); success apenas quando todos os demais contextos do workflow estiveremsuccess(skipped posta como success, observado). ReusalistCommitStatusesdoforgejo-api.mjs.scripts/ci-rollup-check.mjs(novo, CLI plain Node, padrãoforgejo-pr-automerge.mjs):--sha <head-sha> --prefix "CI (PR) / " --own-prefix "CI (PR) / checks"(noci.yml:"CI / "/"CI / checks"); imprime os contextos que falharam e sai 1; verde → 0..forgejo/workflows/ci-pr.yml(jobchecks):if:ganha o fork guard (github.event.pull_request.head.repo.full_name == github.repository, pré-checkout — mesmo padrão doready-automerge);env: FORGEJO_API_TOKEN; steps ganhamactions/checkout@v5+actions/setup-node@v5(node 24,package-manager-cache: false) +node scripts/ci-rollup-check.mjs --sha "${{ github.event.pull_request.head.sha }}" --prefix "CI (PR) / " --own-prefix "CI (PR) / checks"; o passo jq doneedssai (o script o substitui — fonte única).needseif: always()permanecem (o job só roda depois da cascata toda — a ordenação que garante "assentou")..forgejo/workflows/ci.yml(jobchecks): idem, sem fork guard (evento push/dispatch não tem fork);--sha "${{ github.sha }}", prefixoCI /. Com isso odeploy(queneeds: [checks]) nunca roda com shard de e2e vermelho em main.tests/unit/forgejoApi.unit.spec.ts: novo caso pinando o cenário run-607 — rollupCI (PR) / checks= success comCI (PR) / e2e (1)= failure →waitForCheckslança "Checks falharam" (o CLI nunca confia no rollup sozinho).tests/unit/ciRollupCheck.unit.spec.ts(novo): veredito puro — (a) shard vermelho + rollup próprio pending → fail (o run 607 do passado, agora no rollup); (b) shard (2) vermelho → fail (run 716); (c) tudo success → ok; (d) contexto do safety net (PR Ready + auto-merge) pending → ignorado (não casa o prefixo); (e) próprio contexto pending → ignorado; (f) lista vazia → fail; (g) varianteCI /do main; (h)skippedpostado como success → ok.- Docs:
.agents/rules/agent-pr-workflow.mdc(seção "Required checks": rollup agora verifica os statuses individuais via API — o parent de matrix do Forgejo não agrega; nota do fork guard: PRs de fork ficam sem o statuschecks→ merges de fork bloqueados pelo servidor, hardening intencional),docs/AGENT-OPS.md(linha da branch protection + tabela de workflows), header dos dois workflows,docs/changelog/2026-08-18-ops64.md+pnpm changelog:build. - Migration: sem migration (não toca schema/DB).
- Access / Consent: N/A.
- UI: N/A (A).
Fases verificáveis
- Script + veredito puro —
scripts/lib/ci-rollup-check.mjs+ CLI + unit tests;pnpm test:unitlocal. - Workflows —
ci-pr.ymleci.ymlcom o novo passo; diff completo no PR da entrega. - Gates —
pnpm gate:fastlocal (lint/format/typecheck/knip/cycles/unit); e2e roda no CI (OPS59). Push viapnpm push; PRCloses #62. Verificação ao vivo na própria PR: o run doready-automergeda PR prova o primário de novo (já provado no OPS61); o novo rollup proof na PR — se um shard falhar,checksfica vermelho (e o servidor bloqueia), se tudo verde, mergea.
Rabbit holes / Não escopo (engenharia)
- Não trocar poll+merge por
merge_when_checks_succeednativo (achado 5 — não corrige o secundário; poll+merge provado; trocar é item futuro se algum dia o upgrade do Forgejo mudar o contrato). - Não consertar o parent de matrix do Forgejo (bug upstream do runner; a correção é viver sem ele).
- Não refatorar a matrix e2e (OPS34) nem o classificador de escopo.
- Não adicionar o fork guard aos demais jobs do
ci-pr.yml(não têm segredos; fora do escopo). - Não mexer no
waitForChecksalém do teste novo (o CLI já está correto — achado 3).
Riscos e mitigação
- PRs de fork perdem o status
checks(fork guard no job): merges de fork ficam bloqueados pelo servidor (contexto exigido nunca posta). Hardening intencional e alinhado à política atual ("a merged fork PR would deploy" — oready-automergejá exclui forks); documentado no changelog. Se um dia um merge de fork for legítimo, é o escape documentado do OPS61 (ajustar a regra temporariamente). - API indisponível no job
checks→ rollup vermelho: fail-closed (nunca verde por falta de dado); o job vermelha e o PR fica bloqueado até a infra voltar — visível, não silencioso. O mesmo runner self-hosted que roda os jobs serve a API (mesma rede), e oready-automergejá depende dela. - Race status×rollup (status do último shard postado depois do
checkscomeçar): o jobcheckssó roda apósneedsda cascata toda (a ordenação do workflow garante "assentou"), e o runner posta o status no fim de cada job — o window é o mesmo que o jq atual já tinha; owaitForChecks(poll) continua sendo o backstop do merge. - O script roda o código do head no job
checks(same-repo): mesmo nível de confiança doready-automerge(que também roda script do head com o PAT) — same-repo tem write de qualquer forma.
Desfecho (gate com o humano — OPS62 mergeou em main durante a execução)
O OPS62 (mergeado às 23:32Z enquanto este item rodava) reescreveu ci-pr.yml e
ci.yml como jobs únicos sequenciais — sem matrix, sem shards. O bug de
agregação do parent de matrix que este item atacava deixou de existir
estruturalmente: o status do job checks agora É o veredito honesto da
cascata. Decisão do gate: descartar os edits de workflow e o script
ci-rollup-check.mjs (redundantes pós-OPS62), manter o teste-pino do
waitForChecks (run 607: rollup verde + shard vermelho → CLI lança) e as docs
honestas. Entregue: pin + docs + changelog.
Aceite de engenharia
- Aceite de produto da intenção ainda coberto (primário já entregue pelo OPS61; secundário eliminado estruturalmente pelo OPS62; o CLI segue com a defesa individual de statuses, pinada por teste)
- Invariantes AGENTS/engineering-standards (sem código morto — script descartado; docs honestas — a mentira do parent de matrix documentada)
- Testes de domínio previstos (waitForChecks run-607 pinado em
forgejoApi.unit.spec.ts) - Verificação: unit 2360 verdes; lint/format/typecheck/knip/cycles no gate local; a própria PR prova o ciclo (o
ready-automergemergea no fim)