fix(OPS61): gate do waitForChecks sem o pending do próprio job — achado ao vivo PR #67
O Forgejo posta o status do próprio job do safety net como pending durante o run inteiro: o gate antigo (pending.length === 0) era insatisfazível e o CLI nunca auto-mergeou nada — todo merge Forgejo até aqui foi manual (merged_by do dono), inclusive a PR #52 com CI vermelho (narrativa da 'raça' corrigida). O gate é: rollup CI (PR) / checks presente+verde (cascata inteira assentou), ignorando o contexto do próprio workflow; mergeable false só após assentar.
This commit is contained in:
parent
ac11afecf3
commit
8b98930670
6 changed files with 80 additions and 22 deletions
|
|
@ -86,9 +86,14 @@ There is no stage→main promote. Merging to `main` is the delivery; `ci.yml` ve
|
|||
O workflow roda `scripts/forgejo-pr-automerge.mjs` (plain Node, zero-dep): se
|
||||
draft `cursor/*` → marca Ready; espera os checks e mergea por rebase (o
|
||||
equivalente Forgejo do `gh pr merge --auto --rebase`). O wait exige o rollup
|
||||
`CI (PR) / checks` POSTADO e verde (OPS61) — statuses de jobs ainda não
|
||||
agendados não podem parecer verde — e a regra de branch protection no servidor
|
||||
bloqueia o merge POST (405) enquanto o contexto exigido não estiver satisfeito.
|
||||
`CI (PR) / checks` POSTADO e verde (OPS61) — o único contexto que só existe
|
||||
quando a cascata inteira do ci-pr assentou (jobs ainda não agendados não
|
||||
postam status; o próprio status do job do safety net é `pending` durante o run
|
||||
e **nunca** entra no veredito). A regra de branch protection no servidor
|
||||
bloqueia o merge POST (405) enquanto o contexto exigido não estiver satisfeito
|
||||
— inclusive merges manuais via API: foi um merge manual que colocou a PR #52
|
||||
em main com o CI vermelho (o CLI nunca mergeou sozinho antes do OPS61; todo
|
||||
merge Forgejo era manual).
|
||||
Cobre o caso em que o
|
||||
agente (ou o default draft do Cloud) esqueceu e PRs humanas/worktree com merge
|
||||
falho silencioso (incidente OPS52). **Draft de branch não-`cursor/*` nunca é
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ Fast gate: `pnpm gate:fast`. Push: `pnpm push`.
|
|||
| `CURSOR_API_KEY` | Supervisor do pool (Cursor Cloud) |
|
||||
| `POOL_GITHUB_TOKEN` | PAT `actions:write` + `issues:write` (variables do pool; `GITHUB_TOKEN` costuma 403) |
|
||||
|
||||
Branch protection de `main` (aplicada em 2026-08-18, OPS61): required status check `CI (PR) / checks` (o rollup do `ci-pr.yml` — único contexto que cobre a cascata inteira), `strict=false`, 0 reviews. O `waitForChecks` do safety net espera esse mesmo rollup antes de mergear — a regra no servidor é a defesa final (o Forgejo bloqueia o merge POST com 405 se o contexto exigido não estiver verde). Reaplicar/consertar drift: `pnpm configure:branch-protection` (idempotente).
|
||||
Branch protection de `main` (aplicada em 2026-08-18, OPS61): required status check `CI (PR) / checks` (o rollup do `ci-pr.yml` — único contexto que cobre a cascata inteira), `strict=false`, 0 reviews. O `waitForChecks` do safety net espera esse mesmo rollup antes de mergear (o próprio status do job é `pending` durante o run e nunca entra no veredito); a regra no servidor é o gate real — o Forgejo bloqueia o merge POST com 405 se o contexto exigido não estiver verde, inclusive merges manuais via API (a PR #52 entrou em main com CI vermelho por merge manual). Reaplicar/consertar drift: `pnpm configure:branch-protection` (idempotente).
|
||||
|
||||
Deploy (OPS53): o job `deploy` do `ci.yml` roda no host do runner (workstation) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH — build no homeserver (secrets de `~/stack/teqo-1313.env`), registry `localhost:5000`, migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout, smoke pós-deploy. Nenhum secret novo no Forgejo. Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. O deploy Vercel (`vercel-promote.yml`, scripts, `vercel.json`) foi removido no OPS50.
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
**Recently resolved (2026-08-18):** **OPS60** — o sync do `pnpm db:seed:posts` ganhou guard de intenção explícita para escrita no bucket (achado do /simplify da #37): com as envs `S3_*` ativas, o modo sync exige `SEED_MEDIA_CONFIRM=1` (mesmo padrão `isTruthyEnv` do `MEDIA_RECOVER_CONFIRM` da OPS52-media-guard) e recusa com exit 1 antes de qualquer escrita sem a flag — o risco fechado: um worktree com cópia de credenciais `S3_*` de prod (all-or-nothing do `worktree-env.mjs`) rodando sync contra DB local sobrescrevia covers no bucket de **prod** (o seed é idempotente por slug e sobrescreve o key determinístico). Storage local (sem `S3_*` → disk) e `--dry-run` seguem sem flag (não escrevem); echo do alvo ganhou `Bucket`/`Endpoint` quando S3 ativo (paridade com o `media:recover`). Implementação no dono: `isTruthyEnv` (já existente da #37) + `resolveS3StorageEnv` — sem twin. Runbook do OPS58 atualizado com o comando real; nota no AGENTS.md.
|
||||
**Recently resolved (2026-08-18):** **OPS61** — contratos de merge/flip do Forgejo consertados. **(S1)** Os workflows pós-merge (`issue-done-on-main-merge.yml`, `plan-issue-ready-on-main-merge.yml`) rodavam com o `GITHUB_TOKEN` nativo, que 403 em escrita de Issues — o flip `Closes #N → done+in-prod` (PRs #42/#48) quebrava em silêncio (o script engolia o 403 e o job terminava success; flip manual necessário). Agora injetam `FORGEJO_API_TOKEN` (o PAT que o pool já usa para o mesmo tipo de escrita) via `env:` no job, e o `forgejo-issue-transition.mjs` sai **exit 1** se qualquer flip falhar — o job vermelha em vez de mentir. **Fork guard** nos dois workflows (`head.repo.full_name == github.repository`, a mesma classe do `agent-pr-ready-automerge`): PR de fork mergeada faria checkout do script do fork rodando com o PAT do repo (RCE). **(S3)** A branch protection de `main` **não existia no servidor** (0 regras no `GET /branch_protections` — o `pnpm configure:branch-protection` era passo humano pendente do OPS50 e nunca rodou; as docs diziam "aplicado" e mentiam), e o valor `checks` do script **nunca casaria** os contextos reais do Forgejo (casamento por glob contra `CI (PR) / checks (pull_request)` — o sufixo ` (evento)` faz parte do contexto; literal `checks` = todo merge bloqueado). Script corrigido: contexto `CI (PR) / checks*`, idempotente (GET → POST/PATCH/no-op, verifica após aplicar, dry-run honesto) com decisão pura em `scripts/lib/branch-protection.mjs` + spec nova. **(2b, raça do waitForChecks — evidência PR #52:** mergeou com o CI vermelho porque um snapshot com statuses verdes mas jobs ainda não-agendados (sem status) parecia completo; o `docs-guards` postou failure depois): `waitForChecks` passa a exigir o rollup `CI (PR) / checks` **presente e verde** (o único contexto que só existe quando a cascata inteira do ci-pr assentou), `mergeable === false` só conta como conflito após a cascata assentar, timeouts sobem (CLI 30→45 min, workflow 40→55 min — o wait agora cobre e2e/int/build de verdade). A regra no servidor é a defesa final (o merge POST é bloqueado com 405 enquanto o contexto exigido não estiver verde — confirmado na fonte do Forgejo). **Validação ao vivo na própria entrega:** a PR desta entrega (`Closes #53`) mergeou pelo novo gate do rollup com a regra recém-aplicada no servidor, e o `issue-done` flipou a #53 para `done`+`in-prod` com o PAT. Sem migration/access/Consent/UI. Testes: `forgejoApi.unit.spec.ts` (raça do rollup, rollup failure, mergeable pré-settle — fixtures dos waitForChecks/autoMerge existentes atualizados para o contexto real), `branchProtection.unit.spec.ts` nova (create/update/noop, glob casa o contexto observado). (ops(OPS61): flip de issues pós-merge com PAT + gate do rollup checks + branch protection real no Forgejo)
|
||||
|
||||
**Recently resolved (2026-08-18):** **OPS61** — contratos de merge/flip do Forgejo consertados. **(S1)** Os workflows pós-merge (`issue-done-on-main-merge.yml`, `plan-issue-ready-on-main-merge.yml`) rodavam com o `GITHUB_TOKEN` nativo, que 403 em escrita de Issues — o flip `Closes #N → done+in-prod` (PRs #42/#48) quebrava em silêncio (o script engolia o 403 e o job terminava success; flip manual necessário). Agora injetam `FORGEJO_API_TOKEN` (o PAT que o pool já usa para o mesmo tipo de escrita) via `env:` no job, e o `forgejo-issue-transition.mjs` sai **exit 1** se qualquer flip falhar — o job vermelha em vez de mentir. **Fork guard** nos dois workflows (`head.repo.full_name == github.repository`, a mesma classe do `agent-pr-ready-automerge`): PR de fork mergeada faria checkout do script do fork rodando com o PAT do repo (RCE). **(S3)** A branch protection de `main` **não existia no servidor** (0 regras no `GET /branch_protections` — o `pnpm configure:branch-protection` era passo humano pendente do OPS50 e nunca rodou; as docs diziam "aplicado" e mentiam), e o valor `checks` do script **nunca casaria** os contextos reais do Forgejo (casamento por glob contra `CI (PR) / checks (pull_request)` — o sufixo ` (evento)` faz parte do contexto; literal `checks` = todo merge bloqueado). Script corrigido: contexto `CI (PR) / checks*`, idempotente (GET → POST/PATCH/no-op, verifica após aplicar, dry-run honesto) com decisão pura em `scripts/lib/branch-protection.mjs` + spec nova. **(2b — achado ao vivo na própria entrega, PR #67)** O gate antigo do `waitForChecks` (`pending.length === 0`) era **insatisfazível**: o Forgejo posta o status do próprio job do safety net como `pending` durante o run inteiro, então o CLI **nunca auto-mergeou nenhuma PR** — todo merge Forgejo até aqui foi manual (`merged_by` do dono), inclusive o da PR #52 com o CI vermelho (a narrativa da "raça do snapshot" da intenção estava errada: o merge vermelho foi manual, não do script). O gate novo espera o rollup `CI (PR) / checks` presente+verde (único contexto que só existe quando a cascata inteira do ci-pr assentou) e **ignora o próprio contexto** do safety net; `mergeable === false` só conta como conflito após a cascata assentar; timeouts sobem (CLI 30→45 min, workflow 40→55 min). A regra no servidor é o gate real para todo mundo: o merge POST é bloqueado com 405 enquanto o contexto exigido não estiver verde — inclusive merges manuais via API (a PR #52 não teria entrado vermelha). **Validação ao vivo na própria entrega:** a regra foi criada e verificada no servidor (re-run = no-op), e a PR #67 (`Closes #53`) mergeou pelo novo gate do rollup com a regra aplicada; o `issue-done` flipou a #53 para `done`+`in-prod` com o PAT. Sem migration/access/Consent/UI. Testes: `forgejoApi.unit.spec.ts` (rollup não-posto continua poll, rollup failure lança, próprio status do safety net não bloqueia, mergeable pré-settle — fixtures dos waitForChecks/autoMerge existentes atualizados para o contexto real), `branchProtection.unit.spec.ts` nova (create/update/noop, glob casa o contexto observado).
|
||||
|
||||
**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):** **OPS61** — contratos de merge/flip do Forgejo consertados. **(S1)** Os workflows pós-merge (`issue-done-on-main-merge.yml`, `plan-issue-ready-on-main-merge.yml`) rodavam com o `GITHUB_TOKEN` nativo, que 403 em escrita de Issues — o flip `Closes #N → done+in-prod` (PRs #42/#48) quebrava em silêncio (o script engolia o 403 e o job terminava success; flip manual necessário). Agora injetam `FORGEJO_API_TOKEN` (o PAT que o pool já usa para o mesmo tipo de escrita) via `env:` no job, e o `forgejo-issue-transition.mjs` sai **exit 1** se qualquer flip falhar — o job vermelha em vez de mentir. **Fork guard** nos dois workflows (`head.repo.full_name == github.repository`, a mesma classe do `agent-pr-ready-automerge`): PR de fork mergeada faria checkout do script do fork rodando com o PAT do repo (RCE). **(S3)** A branch protection de `main` **não existia no servidor** (0 regras no `GET /branch_protections` — o `pnpm configure:branch-protection` era passo humano pendente do OPS50 e nunca rodou; as docs diziam "aplicado" e mentiam), e o valor `checks` do script **nunca casaria** os contextos reais do Forgejo (casamento por glob contra `CI (PR) / checks (pull_request)` — o sufixo ` (evento)` faz parte do contexto; literal `checks` = todo merge bloqueado). Script corrigido: contexto `CI (PR) / checks*`, idempotente (GET → POST/PATCH/no-op, verifica após aplicar, dry-run honesto) com decisão pura em `scripts/lib/branch-protection.mjs` + spec nova. **(2b, raça do waitForChecks — evidência PR #52:** mergeou com o CI vermelho porque um snapshot com statuses verdes mas jobs ainda não-agendados (sem status) parecia completo; o `docs-guards` postou failure depois): `waitForChecks` passa a exigir o rollup `CI (PR) / checks` **presente e verde** (o único contexto que só existe quando a cascata inteira do ci-pr assentou), `mergeable === false` só conta como conflito após a cascata assentar, timeouts sobem (CLI 30→45 min, workflow 40→55 min — o wait agora cobre e2e/int/build de verdade). A regra no servidor é a defesa final (o merge POST é bloqueado com 405 enquanto o contexto exigido não estiver verde — confirmado na fonte do Forgejo). **Validação ao vivo na própria entrega:** a PR desta entrega (`Closes #53`) mergeou pelo novo gate do rollup com a regra recém-aplicada no servidor, e o `issue-done` flipou a #53 para `done`+`in-prod` com o PAT. Sem migration/access/Consent/UI. Testes: `forgejoApi.unit.spec.ts` (raça do rollup, rollup failure, mergeable pré-settle — fixtures dos waitForChecks/autoMerge existentes atualizados para o contexto real), `branchProtection.unit.spec.ts` nova (create/update/noop, glob casa o contexto observado).
|
||||
**Recently resolved (2026-08-18):** **OPS61** — contratos de merge/flip do Forgejo consertados. **(S1)** Os workflows pós-merge (`issue-done-on-main-merge.yml`, `plan-issue-ready-on-main-merge.yml`) rodavam com o `GITHUB_TOKEN` nativo, que 403 em escrita de Issues — o flip `Closes #N → done+in-prod` (PRs #42/#48) quebrava em silêncio (o script engolia o 403 e o job terminava success; flip manual necessário). Agora injetam `FORGEJO_API_TOKEN` (o PAT que o pool já usa para o mesmo tipo de escrita) via `env:` no job, e o `forgejo-issue-transition.mjs` sai **exit 1** se qualquer flip falhar — o job vermelha em vez de mentir. **Fork guard** nos dois workflows (`head.repo.full_name == github.repository`, a mesma classe do `agent-pr-ready-automerge`): PR de fork mergeada faria checkout do script do fork rodando com o PAT do repo (RCE). **(S3)** A branch protection de `main` **não existia no servidor** (0 regras no `GET /branch_protections` — o `pnpm configure:branch-protection` era passo humano pendente do OPS50 e nunca rodou; as docs diziam "aplicado" e mentiam), e o valor `checks` do script **nunca casaria** os contextos reais do Forgejo (casamento por glob contra `CI (PR) / checks (pull_request)` — o sufixo ` (evento)` faz parte do contexto; literal `checks` = todo merge bloqueado). Script corrigido: contexto `CI (PR) / checks*`, idempotente (GET → POST/PATCH/no-op, verifica após aplicar, dry-run honesto) com decisão pura em `scripts/lib/branch-protection.mjs` + spec nova. **(2b — achado ao vivo na própria entrega, PR #67)** O gate antigo do `waitForChecks` (`pending.length === 0`) era **insatisfazível**: o Forgejo posta o status do próprio job do safety net como `pending` durante o run inteiro, então o CLI **nunca auto-mergeou nenhuma PR** — todo merge Forgejo até aqui foi manual (`merged_by` do dono), inclusive o da PR #52 com o CI vermelho (a narrativa da "raça do snapshot" da intenção estava errada: o merge vermelho foi manual, não do script). O gate novo espera o rollup `CI (PR) / checks` presente+verde (único contexto que só existe quando a cascata inteira do ci-pr assentou) e **ignora o próprio contexto** do safety net; `mergeable === false` só conta como conflito após a cascata assentar; timeouts sobem (CLI 30→45 min, workflow 40→55 min). A regra no servidor é o gate real para todo mundo: o merge POST é bloqueado com 405 enquanto o contexto exigido não estiver verde — inclusive merges manuais via API (a PR #52 não teria entrado vermelha). **Validação ao vivo na própria entrega:** a regra foi criada e verificada no servidor (re-run = no-op), e a PR #67 (`Closes #53`) mergeou pelo novo gate do rollup com a regra aplicada; o `issue-done` flipou a #53 para `done`+`in-prod` com o PAT. Sem migration/access/Consent/UI. Testes: `forgejoApi.unit.spec.ts` (rollup não-posto continua poll, rollup failure lança, próprio status do safety net não bloqueia, mergeable pré-settle — fixtures dos waitForChecks/autoMerge existentes atualizados para o contexto real), `branchProtection.unit.spec.ts` nova (create/update/noop, glob casa o contexto observado).
|
||||
|
|
|
|||
|
|
@ -291,19 +291,30 @@ export const createApi = ({ base, token, repository, fetchImpl } = {}) => {
|
|||
}),
|
||||
|
||||
/**
|
||||
* All required statuses on the PR head are success (or a failure exists),
|
||||
* and Forgejo finished computing mergeability. Returns the last PR read.
|
||||
* A draft PR stops the wait (converted mid-run): the caller decides what
|
||||
* "no merge" means — the CLI skips non-`cursor/*` drafts (OPS57 veto).
|
||||
* The CI (PR) cascade rollup is green and Forgejo finished computing
|
||||
* mergeability. Returns the last PR read. A draft PR stops the wait
|
||||
* (converted mid-run): the caller decides what "no merge" means — the CLI
|
||||
* skips non-`cursor/*` drafts (OPS57 veto).
|
||||
*
|
||||
* OPS61 rollup gate: the CI (PR) cascade's aggregate job (`checks`, the
|
||||
* `if: always()` rollup over every ci-pr job) must have POSTED and be
|
||||
* success. Job statuses are posted as jobs finish, so a snapshot with zero
|
||||
* pending can still miss jobs that were not scheduled yet — that race
|
||||
* merged PR #52 with the CI red. The rollup only exists once the whole
|
||||
* cascade settled (it `needs` every job), so its absence keeps the wait
|
||||
* going. `mergeable === false` is only trusted after the cascade settled —
|
||||
* while jobs still run (or the branch-protection rule blocks), Forgejo may
|
||||
* pending can still miss jobs that were not scheduled yet. The rollup only
|
||||
* exists once the whole cascade settled (it `needs` every job), so its
|
||||
* absence keeps the wait going.
|
||||
*
|
||||
* The safety-net's OWN context (`PR Ready + auto-merge / …`) is posted as
|
||||
* `pending` for the whole run and flips only at the end — it must never
|
||||
* gate the verdict. Live finding (PR #67): the pre-OPS61
|
||||
* `pending.length === 0` gate was unsatisfiable for that reason, so the
|
||||
* CLI never auto-merged anything — every Forgejo merge so far was manual
|
||||
* (`merged_by` the owner), which is also what PR #52's red-CI merge was
|
||||
* (manual, not a waitForChecks race). The branch-protection rule on the
|
||||
* server is the real gate for manual merges too: the merge POST is
|
||||
* rejected (405) while the required context is not green.
|
||||
*
|
||||
* `mergeable === false` is only trusted after the cascade settled — while
|
||||
* jobs still run (or the branch-protection rule blocks), Forgejo may
|
||||
* report false without meaning "conflict".
|
||||
*/
|
||||
waitForChecks: async (number, { timeoutMs = 30 * 60 * 1000, pollMs = 15000, log } = {}) => {
|
||||
|
|
@ -312,12 +323,14 @@ export const createApi = ({ base, token, repository, fetchImpl } = {}) => {
|
|||
const pr = await api.getPullRequest(number)
|
||||
if (!pr || pr.state !== 'OPEN' || pr.merged || pr.isDraft) return pr
|
||||
const statuses = await api.listCommitStatuses(pr.head.sha)
|
||||
const rollup = statuses.find((entry) => entry.context.startsWith('CI (PR) / checks'))
|
||||
const verdicts = statuses.filter(
|
||||
(entry) => !entry.context.startsWith('PR Ready + auto-merge'),
|
||||
)
|
||||
const rollup = verdicts.find((entry) => entry.context.startsWith('CI (PR) / checks'))
|
||||
const cascadeSettled = Boolean(rollup && rollup.state === 'success')
|
||||
const failed = statuses.filter(
|
||||
const failed = verdicts.filter(
|
||||
(entry) => entry.state === 'failure' || entry.state === 'error',
|
||||
)
|
||||
const pending = statuses.filter((entry) => entry.state === 'pending')
|
||||
if (failed.length > 0) {
|
||||
const names = failed.map((entry) => entry.context).join(', ')
|
||||
throw new Error(`Checks falharam no PR #${number}: ${names}`)
|
||||
|
|
@ -325,13 +338,13 @@ export const createApi = ({ base, token, repository, fetchImpl } = {}) => {
|
|||
if (pr.mergeable === false && cascadeSettled) {
|
||||
throw new Error(`PR #${number} não mergeável (conflito?) — resolver manualmente.`)
|
||||
}
|
||||
if (cascadeSettled && pending.length === 0 && pr.mergeable === true) return pr
|
||||
if (cascadeSettled && pr.mergeable === true) return pr
|
||||
if (Date.now() - started > timeoutMs) {
|
||||
throw new Error(`Timeout esperando checks do PR #${number}.`)
|
||||
}
|
||||
if (log)
|
||||
log(
|
||||
`[pr#${number}] ${statuses.length} status, ${pending.length} pendente, rollup=${
|
||||
`[pr#${number}] ${verdicts.length} status, rollup=${
|
||||
rollup ? rollup.state : 'ausente'
|
||||
}, mergeable=${pr.mergeable ?? 'computando'} — aguardando…`,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ describe('forgejo-api', () => {
|
|||
expect(pr.isDraft).toBe(true)
|
||||
})
|
||||
|
||||
it('waitForChecks keeps polling while the CI (PR) checks rollup has not posted (OPS61 race)', async () => {
|
||||
it('waitForChecks keeps polling while the CI (PR) checks rollup has not posted (OPS61 gate)', async () => {
|
||||
let statusPoll = 0
|
||||
const api = createApi({
|
||||
token: 'tok',
|
||||
|
|
@ -283,8 +283,8 @@ describe('forgejo-api', () => {
|
|||
// First snapshots: every POSTED status is green — but jobs that were
|
||||
// not scheduled yet (docs-guards/unit/checks) posted nothing. The
|
||||
// rollup is the only context that exists once the WHOLE cascade
|
||||
// settled; without the gate this snapshot looks green and merges red
|
||||
// (the PR #52 incident).
|
||||
// settled; without the gate a snapshot can look green before the
|
||||
// cascade finished.
|
||||
const statuses =
|
||||
statusPoll < 3
|
||||
? [{ context: 'CI / static', status: 'success' }]
|
||||
|
|
@ -327,6 +327,44 @@ describe('forgejo-api', () => {
|
|||
await expect(api.waitForChecks(4, { pollMs: 1 })).rejects.toThrow(/Checks falharam/)
|
||||
})
|
||||
|
||||
it('waitForChecks ignores the safety-net own status, pending for the whole run (OPS61 finding)', async () => {
|
||||
let statusPoll = 0
|
||||
const api = createApi({
|
||||
token: 'tok',
|
||||
fetchImpl: async (url) => {
|
||||
if (String(url).includes('/pulls/')) {
|
||||
return ok({
|
||||
number: 4,
|
||||
state: 'open',
|
||||
merged: false,
|
||||
mergeable: true,
|
||||
head: { ref: 'x', sha: 'abc' },
|
||||
})
|
||||
}
|
||||
statusPoll += 1
|
||||
// Forgejo posts the job's own commit status as pending at job start
|
||||
// and flips it only at the end — the pre-OPS61 `pending.length === 0`
|
||||
// gate was unsatisfiable and no PR was ever merged by the CLI (every
|
||||
// Forgejo merge was manual). The own context must never gate.
|
||||
return ok({
|
||||
statuses: [
|
||||
{
|
||||
context: 'PR Ready + auto-merge / ready-automerge (pull_request)',
|
||||
status: 'pending',
|
||||
},
|
||||
{ context: 'CI / static', status: 'success' },
|
||||
{ context: 'CI (PR) / checks (pull_request)', status: 'success' },
|
||||
],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const pr = await api.waitForChecks(4, { pollMs: 1 })
|
||||
|
||||
expect(pr).toBeTruthy()
|
||||
expect(statusPoll).toBe(1)
|
||||
})
|
||||
|
||||
it('waitForChecks does not treat mergeable=false as conflict before the rollup settles (OPS61)', async () => {
|
||||
let poll = 0
|
||||
const api = createApi({
|
||||
|
|
|
|||
Loading…
Reference in a new issue