From add96aff7c245c3b3dcc597d661c68c24a0d3005 Mon Sep 17 00:00:00 2001 From: rubenmarcus Date: Fri, 21 Aug 2026 23:43:08 +0100 Subject: [PATCH 1/2] =?UTF-8?q?fix(ci):=20quem=20bloqueia=20o=20PR=20?= =?UTF-8?q?=C3=A9=20o=20build=20do=20CI,=20n=C3=A3o=20a=20Vercel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MEDIDO nos últimos 29 PRs: a Vercel reprovou 13 dos 14 PRs vindos de fork, sempre com `Authorization required to deploy.` - proteção de fork da própria Vercel, que nenhum commit do colaborador resolve. Nos PRs de casa o vermelho era build de verdade, uma vez porque o `fetch-decals.sh` não baixou o acervo (196 de 197 decalques faltando) num soluço de rede. O `ci.yml` já constrói o site em `pull_request`: o portão que importa existe e é esse. A régua nova guarda as três condições que deixam a Vercel sair do caminho crítico sem perder cobertura: DG1 o CI constrói no PR - sem isso, tirar a Vercel do bloqueio deixaria o PR sem nenhuma prova de que o site compila; DG2 nenhum `curl` do caminho do build corre sem `--retry`. `set -e` mais curl sem retry transforma soluço de rede em PR vermelho, e o autor não tem o que consertar - mesmo dano do portão que não pode medir; DG3 a etiqueta que autoriza o preview de fork é criada por quem depende dela. A DG3 achou código morto: o job `preview` do preview-bot já sabe publicar pela CLI da Vercel depois de conferir permissão do ator e SHA aprovado, mas dispara em `labeled` com `preview-autorizado` - e essa etiqueta NUNCA existiu no repositório. O bot pedia um rótulo que ninguém tinha como aplicar. Agora o próprio workflow a cria. Os dois ajustes que sobram são de painel da Vercel e não cabem no repositório; ficaram escritos no PROD-READINESS.md, com o que medir. Co-Authored-By: Claude Opus 5 Signed-off-by: rubenmarcus Agent: Claude Code --- .github/workflows/ci.yml | 1 + .github/workflows/preview-bot.yml | 8 ++ ARCH.generated.md | 6 +- PROD-READINESS.md | 24 ++++++ README.md | 2 +- STATUS.md | 2 +- docs/docs/comecando.md | 6 +- docs/docs/quality-gates.md | 2 +- .../current/comecando.md | 6 +- .../current/quality-gates.md | 2 +- package.json | 7 +- scripts/ci/ensure_labels.py | 43 +++++++++-- scripts/fetch-audio.sh | 2 +- scripts/fetch-decals.sh | 2 +- tools/eval/deploy-gate-check.mjs | 77 +++++++++++++++++++ tools/eval/fixture-check.mjs | 1 - 16 files changed, 166 insertions(+), 25 deletions(-) create mode 100644 tools/eval/deploy-gate-check.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index daeff137d..99143049a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,7 @@ jobs: # Régua se prova antes de medir o PR: portão quebrado passa calado. - name: Autoteste das réguas de CI run: | + python3 scripts/ci/ensure_labels.py --selftest python3 scripts/ci/dco_check.py --selftest python3 scripts/ci/agente_check.py --selftest - name: Check Signed-off-by diff --git a/.github/workflows/preview-bot.yml b/.github/workflows/preview-bot.yml index 4d8fae77f..034fc734e 100644 --- a/.github/workflows/preview-bot.yml +++ b/.github/workflows/preview-bot.yml @@ -26,6 +26,14 @@ jobs: REPO: ${{ github.repository }} run: gh pr edit "$PR" --repo "$REPO" --remove-label "preview-autorizado" 2>/dev/null || true + # O job `preview` abaixo só dispara em `labeled` com esta etiqueta. Ela nunca foi + # criada no repositório, então o caminho inteiro era código morto: o bot pedia um + # rótulo que ninguém tinha como aplicar. Criar aqui é o que liga o mecanismo. + - name: garante a etiqueta que autoriza o preview + env: + GH_TOKEN: ${{ github.token }} + run: python3 scripts/ci/ensure_labels.py preview-autorizado + - name: classifica o diff sem executar código do fork env: GH_TOKEN: ${{ github.token }} diff --git a/ARCH.generated.md b/ARCH.generated.md index 49b5134b7..58baefc7a 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -10,7 +10,7 @@ Números atuais das zonas, do quality gate e do `package.json`: |---|---|---|---| | `public/` | o **jogo** | 44 arquivos `.js`, 31.956 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | | `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | -| `tools/` | o **arnês** | 195 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | +| `tools/` | o **arnês** | 196 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. @@ -25,10 +25,10 @@ Comandos e scripts atuais do quality gate: ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:preload eval:docsautoria eval:replaycam +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam ``` -`package.json` tem **119 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **120 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/PROD-READINESS.md b/PROD-READINESS.md index badd352c1..a7628e722 100644 --- a/PROD-READINESS.md +++ b/PROD-READINESS.md @@ -26,6 +26,30 @@ - Headers de cache: `/vendor/*` curto, `/models/*` e `/audio/a/*` longo/immutable, `og-image.png` 7 dias. - CSP está declarado em `vercel.json`; import map e scripts são `self` + `unsafe-inline` + Cloudflare beacon. +## Preview de PR e a Vercel (21/08/2026) + +Medido nos últimos 29 PRs: a Vercel reprovou **13 dos 14 PRs vindos de fork**, sempre com +`Authorization required to deploy.` - é a proteção de fork da própria Vercel, e nenhum +commit do colaborador a resolve. + +**Quem bloqueia o PR é o `ci.yml`**, que já roda `npm run build` em `pull_request`. A +Vercel é conveniência, não portão - a régua `eval:deploygate` (DG1) guarda essa condição. + +Dois ajustes ficam no **painel da Vercel**, fora do alcance do repositório, e precisam da +conta dona do projeto: + +1. **Project → Git → Deploy Hooks / Fork Protection**: desligar o deploy automático de PR + vindo de fork. Enquanto estiver ligado, todo PR externo nasce com um vermelho que o + autor não tem como consertar. +2. **Settings → Git → Ignored Build Step**: opcional, para parar de gastar build em branch + de PR interna. A produção continua publicando pela `main`. + +O preview de fork continua disponível **sob demanda**: um mantenedor aplica a etiqueta +`preview-autorizado` e o job `preview` do `preview-bot.yml` publica pela CLI da Vercel, +depois de conferir permissão do ator e SHA aprovado. A etiqueta passou a ser criada pelo +próprio workflow (DG3) - antes ela não existia no repositório e o caminho inteiro era +código morto. + ## Blockers conhecidos para prod 1. **`npm run check` (full) não foi executado nesta sessão.** `check:fast` passou; `check` ainda inclui `eval:vm`, `invariants`, `kick`, `bots` e deve ser verde antes de publicar. diff --git a/README.md b/README.md index f5ebe1bac..9408f5921 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 195 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 196 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.172` | `public/js/version.js` e `package.json` (batem) | diff --git a/STATUS.md b/STATUS.md index b4fc41129..a77a4c3f8 100644 --- a/STATUS.md +++ b/STATUS.md @@ -5,7 +5,7 @@ - **Versão:** `2.0.0-alpha.172` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB - **Código do jogo:** 31.956 linhas em 44 módulos JavaScript -- **Automação:** 119 comandos npm, 195 scripts de avaliação e 54 scripts de pipeline +- **Automação:** 120 comandos npm, 196 scripts de avaliação e 54 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 5ad8e4f16..ad83ad10c 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -49,7 +49,7 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 195 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 196 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.172` | `public/js/version.js` e `package.json` (batem) | @@ -275,10 +275,10 @@ E os dois quality gates, com a lista exata do que cada um roda — direto do `pa {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:preload eval:docsautoria eval:replaycam +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam ``` -`package.json` tem **119 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **120 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index c43d171a8..e5f888a82 100644 --- a/docs/docs/quality-gates.md +++ b/docs/docs/quality-gates.md @@ -15,7 +15,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2.275 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **195 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. +- O arnês inteiro são **196 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. - Quantas invariantes rodam como **críticas** numa execução **não é derivável do fonte**: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do `KNOWN-BUGS.md`, atualizado com saída real. Reproduza: diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index b959a5a7e..9402aca2c 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -51,7 +51,7 @@ this page was aging at the very first commit — see | Playable characters | 44, in 5 factions | `CHARACTERS` array in `characters.js` | | Maps in the registry | 12 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 195 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 196 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Version | `2.0.0-alpha.172` | `public/js/version.js` and `package.json` (match) | @@ -277,10 +277,10 @@ And the two gates, with the exact list of what each one runs — straight from ` {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:preload eval:docsautoria eval:replaycam +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam ``` -`package.json` has **119 scripts**; the reason behind each one lives in `SCRIPTS.md`. +`package.json` has **120 scripts**; the reason behind each one lives in `SCRIPTS.md`. > Block generated by `node tools/gen-docs.mjs`. Source: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md index bd07de931..52444e462 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md @@ -18,7 +18,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2,275 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **195 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. +- The harness contains **196 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. - The number of critical checks in one run depends on the inputs present on that machine; dated results belong in `KNOWN-BUGS.md`. ```bash diff --git a/package.json b/package.json index b67572109..9d6c196b8 100644 --- a/package.json +++ b/package.json @@ -118,11 +118,12 @@ "eval:regen": "node tools/eval/regen-check.mjs", "eval:ctfwin": "node tools/eval/ctf-win-check.mjs", "eval:spawn": "node tools/eval/spawn-settle-check.mjs", - "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters eval:comentario eval:fixture eval:wfsecret eval:preload eval:docsautoria eval:replaycam", - "check:fast": "node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:preload eval:docsautoria eval:replaycam", + "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam", + "check:fast": "node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam", "eval:tags": "node tools/eval/tag-main-check.mjs", "eval:simclock": "node tools/eval/sim-clock-check.mjs", - "eval:wfsecret": "node tools/eval/workflow-secret-check.mjs" + "eval:wfsecret": "node tools/eval/workflow-secret-check.mjs", + "eval:deploygate": "node tools/eval/deploy-gate-check.mjs" }, "repository": { "type": "git", diff --git a/scripts/ci/ensure_labels.py b/scripts/ci/ensure_labels.py index 6810e02e0..e515c34ae 100644 --- a/scripts/ci/ensure_labels.py +++ b/scripts/ci/ensure_labels.py @@ -17,18 +17,49 @@ "crash-auto": ("B60205", "crash de produção reportado por /api/jserror ou prod-watch"), "stale-backlog": ("c2e0c6", "Issue antiga ou desalinhada com o checkout atual; precisa revalidação"), "needs-repro": ("f9d0c4", "Issue precisa passos de reprodução mais concretos"), + "preview-autorizado": ("0e8a16", "Mantenedor revisou o SHA atual e liberou preview do fork na Vercel"), } -def main() -> int: - for label in sys.argv[1:]: +def comandos(nomes: list[str]) -> list[list[str]]: + """Os `gh label create` que ESTE pedido produz. + + Nome fora do dicionário some em silêncio, e foi assim que `preview-autorizado` + passou meses sendo pedida por um workflow sem nunca ser criada — o job de preview + de fork inteiro virou código morto atrás de um rótulo que ninguém podia aplicar. + """ + out = [] + for label in nomes: if label not in LABELS: continue color, description = LABELS[label] - subprocess.run( - ["gh", "label", "create", label, "--color", color, "--description", description, "--force"], - check=True, - ) + out.append(["gh", "label", "create", label, "--color", color, "--description", description, "--force"]) + return out + + +def selftest() -> int: + casos = [ + ("rótulo conhecido vira um comando", ["safe-automerge"], ["safe-automerge"]), + ("desconhecido é ignorado", ["nao-existe"], []), + ("conhecido junto de desconhecido não contamina", ["nao-existe", "bot-fixable"], ["bot-fixable"]), + ("lista vazia não faz nada", [], []), + ("preview-autorizado está registrado", ["preview-autorizado"], ["preview-autorizado"]), + ] + erros = 0 + for nome, pedido, esperado in casos: + obtido = [c[3] for c in comandos(pedido)] + ok = obtido == esperado + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} {nome}: {obtido!r}") + print("selftest verde" if not erros else f"{erros} caso(s) vermelho(s)") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + for comando in comandos(sys.argv[1:]): + subprocess.run(comando, check=True) return 0 diff --git a/scripts/fetch-audio.sh b/scripts/fetch-audio.sh index 58d27b672..3dd9e6b25 100755 --- a/scripts/fetch-audio.sh +++ b/scripts/fetch-audio.sh @@ -18,7 +18,7 @@ if [ -f "$DEST/manifest.json" ]; then fi mkdir -p "$DEST" echo "Baixando pacote de áudio de: $URL" -curl -fsSL "$URL" -o /tmp/csbrasil-audio.zip +curl --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 20 -fsSL "$URL" -o /tmp/csbrasil-audio.zip unzip -o -q /tmp/csbrasil-audio.zip -d "$DEST/" [ -f "$DEST/manifest.json" ] || cp "$DEST/manifest.example.json" "$DEST/manifest.json" echo "Pronto. Áudio instalado em $DEST/." diff --git a/scripts/fetch-decals.sh b/scripts/fetch-decals.sh index b9697f17b..dcafbcf85 100755 --- a/scripts/fetch-decals.sh +++ b/scripts/fetch-decals.sh @@ -24,6 +24,6 @@ if [ -d "$DEST" ] && [ "$(ls -1 "$DEST"/*.png 2>/dev/null | grep -cv '/or-')" -g fi mkdir -p "$DEST" echo "Baixando decalques de: $URL" -curl -fsSL "$URL" -o /tmp/csbrasil-decals.zip +curl --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 20 -fsSL "$URL" -o /tmp/csbrasil-decals.zip unzip -o -q /tmp/csbrasil-decals.zip -d "$DEST/" echo "Pronto. $(ls -1 "$DEST"/*.png | wc -l | tr -d ' ') decalques em $DEST/." diff --git a/tools/eval/deploy-gate-check.mjs b/tools/eval/deploy-gate-check.mjs new file mode 100644 index 000000000..01b662b7b --- /dev/null +++ b/tools/eval/deploy-gate-check.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +/* ============================================================================ + deploy-gate-check.mjs — QUEM BLOQUEIA O PR É O CI, NÃO A VERCEL + ---------------------------------------------------------------------------- + MEDIDO EM 21/08/2026: Vercel vermelha em 13 dos 14 PRs vindos de fork, sempre + com `Authorization required to deploy.` - a proteção de fork da própria Vercel, + que não se resolve por commit nenhum. E em 4 PRs de casa o vermelho era build + real, uma vez por `fetch-decals.sh` não ter baixado o acervo (196 de 197 + decalques faltando) num soluço de rede. + + Para poder tirar a Vercel do caminho crítico do PR, duas coisas precisam valer: + + DG1 · o CI constrói o site no próprio PR. Sem isso, tirar a Vercel do bloqueio + deixaria o PR sem NENHUMA prova de que o site compila. + DG2 · nenhum download do caminho do build corre sem retry. `set -e` mais um + `curl` sem `--retry` transforma soluço de rede em PR vermelho, e o autor + não tem o que consertar - é o mesmo dano do portão que não pode medir. + DG3 · a etiqueta que autoriza o preview de fork é criada por quem depende dela. + O job existia e a etiqueta não: o bot pedia um rótulo que ninguém podia + aplicar, e o caminho de preview era código morto. + + Uso: node tools/eval/deploy-gate-check.mjs [--mutante=] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { 'ci-sem-build': 'DG1', 'download-sem-retry': 'DG2', 'preview-sem-etiqueta': 'DG3', 'etiqueta-nao-registrada': 'DG3' }; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const ler = (p) => { try { return readFileSync(p, 'utf8'); } catch { return ''; } }; +const falhas = []; + +/* ---- DG1: o CI constrói no PR ---- */ +let ci = ler('.github/workflows/ci.yml'); +if (MUT === 'ci-sem-build') ci = ci.replace('run: npm run build', 'run: echo pulei o build'); +const on = (ci.match(/^on:\n([\s\S]*?)(?=^\S)/m) || [])[1] || ''; +if (!/^\s{2}pull_request:/m.test(on) || !/run:\s*npm run build/.test(ci)) { + falhas.push('DG1 ci.yml não constrói o site em pull_request — sem isso a Vercel não pode sair do bloqueio'); +} + +/* ---- DG2: download do build com retry ---- */ +const FETCHERS = readdirSync('scripts').filter((f) => /^fetch-.*\.sh$/.test(f)); +for (const f of FETCHERS) { + let sh = ler(`scripts/${f}`); + if (MUT === 'download-sem-retry' && f === 'fetch-decals.sh') { + sh = sh.replace(/curl --retry \d+[^"]*-fsSL/, 'curl -fsSL'); + } + for (const linha of sh.split('\n')) { + if (!/^\s*curl\s/.test(linha)) continue; + if (!/--retry\b/.test(linha)) { + falhas.push(`DG2 scripts/${f}: curl sem --retry no caminho do build — soluço de rede vira PR vermelho`); + } + } +} +if (!FETCHERS.length) falhas.push('DG2 nenhum scripts/fetch-*.sh encontrado — a régua perdeu o alvo'); + +/* ---- DG3: a etiqueta do preview nasce com quem a usa ---- */ +let preview = ler('.github/workflows/preview-bot.yml'); +if (MUT === 'preview-sem-etiqueta') preview = preview.replace(/ensure_labels\.py preview-autorizado/, 'ensure_labels.py'); +const usaEtiqueta = /github\.event\.label\.name == 'preview-autorizado'/.test(preview); +const criaEtiqueta = /ensure_labels\.py[^\n]*\bpreview-autorizado\b/.test(preview); +/* Chamar o ensure_labels não basta: ele IGNORA em silêncio qualquer nome que não esteja + no dicionário LABELS dele. Pedir a criação sem registrar o rótulo é o mesmo código + morto de antes, só que mais difícil de enxergar. */ +let labels = ler('scripts/ci/ensure_labels.py'); +if (MUT === 'etiqueta-nao-registrada') labels = labels.replace(/^\s*"preview-autorizado".*$/m, ''); +const registraEtiqueta = /"preview-autorizado":\s*\(/.test(labels); +if (usaEtiqueta && !criaEtiqueta) { + falhas.push('DG3 preview-bot.yml exige a etiqueta `preview-autorizado` e não a cria — o caminho de preview vira código morto'); +} else if (usaEtiqueta && !registraEtiqueta) { + falhas.push('DG3 `preview-autorizado` é pedida ao ensure_labels.py mas não está no dicionário LABELS dele — a criação é ignorada em silêncio'); +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(` \x1b[32m✓\x1b[0m DG o PR é bloqueado pelo build do CI; download tem retry (${FETCHERS.length} fetchers); etiqueta de preview existe`); +if (MUT && !falhas.length) console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego`); +process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/fixture-check.mjs b/tools/eval/fixture-check.mjs index 987f43be2..a10fd1b42 100644 --- a/tools/eval/fixture-check.mjs +++ b/tools/eval/fixture-check.mjs @@ -35,7 +35,6 @@ const base = arg('base') || 'origin/main'; /* Dívida herdada: script de decisão que nasceu sem fixture. A lista só encolhe. Quem tocar num destes escreve o `--selftest` no mesmo PR. */ const SEM_FIXTURE = new Set([ - 'scripts/ci/ensure_labels.py', 'scripts/ci/issue_triage.py', 'scripts/ci/pr_classify.py', 'scripts/ci/pr_route.py', From 9e3be1a0c1dc0a585daa6eceb93f09081e997cb3 Mon Sep 17 00:00:00 2001 From: Ruben Date: Sat, 22 Aug 2026 00:29:48 +0100 Subject: [PATCH 2/2] =?UTF-8?q?feat(ci):=20autofix=20conserta=20o=20mec?= =?UTF-8?q?=C3=A2nico,=20com=20lista=20de=20permiss=C3=A3o=20e=20sem=20mer?= =?UTF-8?q?gear=20(#411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ci): autofix conserta o mecânico, com lista de permissão e sem mergear MEDIDO nos últimos 29 PRs: nenhum bot deste repositório jamais commitou um conserto. Quem conserta é sempre uma pessoa. O #405 está vermelho por `DOCS1: documentação DESATUALIZADA`, cujo conserto é `npm run docs` mais um commit - trabalho sem julgamento nenhum que hoje devolve o PR para o autor. O autofix roda em `pull_request_target`, o único gatilho que enxerga secret em PR de fork (é a mesma regra que a WSEC1 passou a guardar), regenera os blocos derivados e empurra o commit. A TRAVA é o que torna isso aceitável: `autofix_allowlist.py` só libera arquivo GERADO, e o commit sai da saída dela - nunca de um `git add -A`. Encostou em qualquer outro caminho, o bot aborta e comenta. Sem a lista, o primeiro conserto errado reescreve o mapa de um colaborador e ninguém repara; com ela, o pior caso é regenerar uma doc que já era derivada. `.github/` está entre os PROIBIDOS de propósito: um bot que edita o workflow que o governa amplia a própria permissão num commit. E as ferramentas que rodam são restauradas da BASE antes de executar - PR nenhum reescreve o gerador e faz o bot executar o que ele mandar. Duas saídas sem push, porque vermelho que o autor não entende é o dano que este trabalho inteiro está atacando: fork com "allow edits by maintainers" desligado recebe o comando por comentário, e sem `CSBRASIL_BOT_TOKEN` o job avisa e sai verde em vez de reprovar. A régua AF1/AF2/AF3 guarda as três condições - escrita só depois da trava, lista sem caminho de código, e o bot NÃO mergeia (deixar pronto e fechar são coisas diferentes). Os quatro mutantes mordem. Co-Authored-By: Claude Opus 5 Signed-off-by: rubenmarcus Agent: Claude Code * fix(ci): bot marca `pronto-pra-merge` em vez de mergear sozinho (#412) A intenção declarada dos bots é autocorreção e autorrevisão para deixar o PR PRONTO - não fechar o assunto. Só que o `csbrasil-bot-automerge` chamava `gh pr merge --squash --delete-branch --auto`, e era a ÚNICA coisa que um bot deste repositório fazia sozinho: justamente a que não devia. O passo agora aplica a etiqueta `pronto-pra-merge` e comenta por que o PR está pronto. Quando o PR deixa de ser elegível, a etiqueta sai - senão ela vira um carimbo velho que ninguém confere. O botão de merge continua humano. A AF4 estende a régua do autofix para o repositório inteiro: nenhum workflow pode chamar `gh pr merge`. Ela ignora linha de comentário de propósito, para que o comentário que explica por que a chamada saiu possa citá-la. O mutante `automerge-volta` recoloca a chamada e reprova. Fora do alcance deste repositório: o Esbirro, que repetiu o mesmo alerta de `bundle-em-dia` a cada 30 minutos por 17 horas na issue #393 sem nunca agir, vive em `game3/bots/vigia-prod/plantao.mjs`. Dar ação a ele é trabalho de lá. Agent: Claude Code Signed-off-by: rubenmarcus Co-authored-by: Claude Opus 5 --------- Signed-off-by: rubenmarcus Co-authored-by: Claude Opus 5 --- .github/workflows/autofix.yml | 147 ++++++++++++++++++ .github/workflows/ci.yml | 1 + .github/workflows/csbrasil-bot-automerge.yml | 19 ++- ARCH.generated.md | 6 +- README.md | 2 +- STATUS.md | 2 +- docs/docs/comecando.md | 6 +- docs/docs/quality-gates.md | 2 +- .../current/comecando.md | 6 +- .../current/quality-gates.md | 2 +- package.json | 7 +- scripts/ci/autofix_allowlist.py | 116 ++++++++++++++ scripts/ci/ensure_labels.py | 1 + tools/eval/autofix-check.mjs | 100 ++++++++++++ 14 files changed, 398 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/autofix.yml create mode 100644 scripts/ci/autofix_allowlist.py create mode 100644 tools/eval/autofix-check.mjs diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 000000000..284f96158 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,147 @@ +# autofix — o bot conserta o MECÂNICO e deixa o PR pronto. Ele NÃO mergeia. +# +# POR QUE EXISTE (medido em 21/08/2026, nos últimos 29 PRs): nenhum bot deste +# repositório jamais commitou um conserto. O #405 ficou vermelho por +# `DOCS1: documentação DESATUALIZADA`, cujo conserto é `npm run docs` mais um +# commit — trabalho sem julgamento nenhum, que hoje ou espera um mantenedor ou +# devolve o PR para o autor. +# +# A TRAVA: `scripts/ci/autofix_allowlist.py`. O bot só commita se TUDO que ele +# mexeu for arquivo gerado. Encostou em qualquer outro caminho, aborta e comenta. +# Sem essa lista, o primeiro conserto errado reescreve o mapa de um colaborador e +# ninguém repara — e a confiança de todo mundo de fora vai junto. +# +# `pull_request_target` porque é o único gatilho que enxerga secret em PR de fork +# (ver a régua WSEC1). O checkout é do MERGE do PR e roda apenas ferramentas do +# repositório base sobre ele — nunca `npm run` de script vindo do fork. +name: autofix + +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + workflow_dispatch: + inputs: + pr_number: + description: PR a consertar + required: true + type: number + +permissions: + contents: read + pull-requests: write + +concurrency: + group: autofix-${{ github.event.pull_request.number || inputs.pr_number }} + cancel-in-progress: true + +jobs: + autofix: + if: github.event.pull_request.draft != true + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + GH_TOKEN: ${{ secrets.CSBRASIL_BOT_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} + REPO: ${{ github.repository }} + steps: + - name: Guard de identidade + run: | + if [ -z "$GH_TOKEN" ]; then + echo "::warning::CSBRASIL_BOT_TOKEN ausente — autofix não roda, e não reprova ninguém por isso." + echo "pular=1" >> "$GITHUB_ENV" + fi + + - name: Dados do PR + if: env.pular != '1' + run: | + gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json headRefName,headRepository,headRepositoryOwner,maintainerCanModify > /tmp/pr.json + { + echo "HEAD_REF=$(jq -r .headRefName /tmp/pr.json)" + echo "HEAD_REPO=$(jq -r '.headRepositoryOwner.login + "/" + .headRepository.name' /tmp/pr.json)" + echo "PODE_EDITAR=$(jq -r .maintainerCanModify /tmp/pr.json)" + } >> "$GITHUB_ENV" + + # Checa o código do PR para poder REGERAR a doc a partir dele. Nada do fork é + # executado: os geradores abaixo são os do repositório base, restaurados logo + # depois do checkout. + - name: Checkout do PR + if: env.pular != '1' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: ${{ env.HEAD_REPO }} + ref: ${{ env.HEAD_REF }} + token: ${{ secrets.CSBRASIL_BOT_TOKEN }} + fetch-depth: 0 + + # As ferramentas que vão rodar têm de ser as da BASE, nunca as do fork — senão + # um PR malicioso reescreve o gerador e o bot executa o que ele mandar. + - name: Restaura as ferramentas da base + if: env.pular != '1' + run: | + git remote add base "https://github.com/$REPO.git" 2>/dev/null || true + git fetch --depth=1 base "${{ github.event.pull_request.base.ref || 'main' }}" + git checkout FETCH_HEAD -- tools/ scripts/ package.json + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + if: env.pular != '1' + with: { node-version: 22 } + - name: Instala dependências + if: env.pular != '1' + run: npm ci --ignore-scripts + + - name: Conserta o mecânico + if: env.pular != '1' + run: | + node tools/gen-docs.mjs || echo "::warning::gen-docs falhou" + node tools/gen-arch.mjs || echo "::warning::gen-arch falhou" + + # As ferramentas foram restauradas da base só para rodar; o que volta ao PR é + # apenas o resultado delas. + - name: Devolve as ferramentas ao estado do PR + if: env.pular != '1' + run: git checkout HEAD -- tools/ scripts/ package.json + + - name: A trava — só arquivo gerado pode virar commit + if: env.pular != '1' + id: trava + run: | + git status --porcelain > /tmp/mexidos.txt + if [ ! -s /tmp/mexidos.txt ]; then + echo "nada=1" >> "$GITHUB_OUTPUT" + echo "PR já está em dia — nada a consertar." + exit 0 + fi + if ! python3 scripts/ci/autofix_allowlist.py < /tmp/mexidos.txt > /tmp/permitidos.txt; then + cat /tmp/permitidos.txt + echo "bloqueado=1" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "commitar=1" >> "$GITHUB_OUTPUT" + + - name: Comenta quando a trava bloqueia + if: env.pular != '1' && steps.trava.outputs.bloqueado == '1' + run: | + gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ + "🤖 **autofix**: o conserto encostou em arquivo fora da lista de permissão, então **não commitei nada**. Rode \`npm run docs && node tools/gen-arch.mjs\` e confira o que mudou." + + - name: Empurra o conserto + if: env.pular != '1' && steps.trava.outputs.commitar == '1' && env.PODE_EDITAR == 'true' + run: | + git config user.name "csbrasil-bot" + git config user.email "csbrasil-bot@users.noreply.github.com" + xargs -a /tmp/permitidos.txt -r git add -- + git commit -m "chore(docs): regenera bloco derivado (autofix)" \ + -m "Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão." \ + -m "Signed-off-by: csbrasil-bot " + git push origin "HEAD:$HEAD_REF" + gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ + "🤖 **autofix**: regenerei os blocos derivados e empurrei o commit. O PR não precisa de mais nada disso." + + # Fork com "allow edits by maintainers" desligado: o bot não tem como empurrar. + # Comentar o comando é melhor que um vermelho que o autor não entende. + - name: Sem permissão de push — comenta o comando + if: env.pular != '1' && steps.trava.outputs.commitar == '1' && env.PODE_EDITAR != 'true' + run: | + gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ + "🤖 **autofix**: dá para consertar sozinho, mas este PR está com *allow edits by maintainers* desligado. Rode \`npm run docs && node tools/gen-arch.mjs\` e commite — ou ligue a opção e eu faço." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99143049a..e8a62c4d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,7 @@ jobs: - name: Autoteste das réguas de CI run: | python3 scripts/ci/ensure_labels.py --selftest + python3 scripts/ci/autofix_allowlist.py --selftest python3 scripts/ci/dco_check.py --selftest python3 scripts/ci/agente_check.py --selftest - name: Check Signed-off-by diff --git a/.github/workflows/csbrasil-bot-automerge.yml b/.github/workflows/csbrasil-bot-automerge.yml index bff31b52c..b4e9afe0f 100644 --- a/.github/workflows/csbrasil-bot-automerge.yml +++ b/.github/workflows/csbrasil-bot-automerge.yml @@ -45,6 +45,8 @@ jobs: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: python-version: '3.13' + - name: Garante a etiqueta de pronto + run: python3 scripts/ci/ensure_labels.py pronto-pra-merge - name: Fetch PR status run: gh pr view "$PR_NUMBER" --json isDraft,labels,reviewDecision,mergeStateStatus,statusCheckRollup,files > /tmp/automerge.json - name: Check eligibility @@ -53,13 +55,24 @@ jobs: run: | python3 scripts/ci/check_automerge.py --selftest python3 scripts/ci/check_automerge.py < /tmp/automerge.json > /tmp/automerge-result.json - - name: Merge when eligible + # DEIXAR PRONTO NÃO É MERGEAR (decisão do dono, 21/08). Este passo chamava + # `gh pr merge --squash --auto` e fechava o PR sozinho. A intenção declarada dos + # bots é outra: autocorreção e autorrevisão para deixar o PR PRONTO, com o botão + # de merge continuando humano. O que era merge vira etiqueta e um comentário que + # diz por que o PR está pronto. + - name: Marca como pronto quando elegível run: | python3 - <<'PY' import json, os, subprocess, sys data = json.load(open('/tmp/automerge-result.json')) + pr = os.environ['PR_NUMBER'] if not data.get('eligible'): - print('automerge: not eligible') + print('pronto-pra-merge: not eligible') + subprocess.run(['gh', 'pr', 'edit', pr, '--remove-label', 'pronto-pra-merge'], check=False) sys.exit(0) - subprocess.run(['gh', 'pr', 'merge', os.environ['PR_NUMBER'], '--squash', '--delete-branch', '--auto'], check=True) + subprocess.run(['gh', 'pr', 'edit', pr, '--add-label', 'pronto-pra-merge'], check=True) + subprocess.run(['gh', 'pr', 'comment', pr, '--body', + '🤖 **csbrasil-bot**: portões verdes e diff dentro do que o ' + '`check_automerge` aceita — marquei como `pronto-pra-merge`. ' + 'O merge continua sendo seu.'], check=True) PY diff --git a/ARCH.generated.md b/ARCH.generated.md index 58baefc7a..fb1dcaa49 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -10,7 +10,7 @@ Números atuais das zonas, do quality gate e do `package.json`: |---|---|---|---| | `public/` | o **jogo** | 44 arquivos `.js`, 31.956 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | | `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | -| `tools/` | o **arnês** | 196 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | +| `tools/` | o **arnês** | 197 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. @@ -25,10 +25,10 @@ Comandos e scripts atuais do quality gate: ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:autofix eval:preload eval:docsautoria eval:replaycam ``` -`package.json` tem **120 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **121 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/README.md b/README.md index 9408f5921..22398b10c 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 196 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 197 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.172` | `public/js/version.js` e `package.json` (batem) | diff --git a/STATUS.md b/STATUS.md index a77a4c3f8..c7df2c117 100644 --- a/STATUS.md +++ b/STATUS.md @@ -5,7 +5,7 @@ - **Versão:** `2.0.0-alpha.172` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB - **Código do jogo:** 31.956 linhas em 44 módulos JavaScript -- **Automação:** 120 comandos npm, 196 scripts de avaliação e 54 scripts de pipeline +- **Automação:** 121 comandos npm, 197 scripts de avaliação e 54 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index ad83ad10c..0c3f07e68 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -49,7 +49,7 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 196 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 197 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.172` | `public/js/version.js` e `package.json` (batem) | @@ -275,10 +275,10 @@ E os dois quality gates, com a lista exata do que cada um roda — direto do `pa {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:autofix eval:preload eval:docsautoria eval:replaycam ``` -`package.json` tem **120 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **121 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index e5f888a82..a5ab9d591 100644 --- a/docs/docs/quality-gates.md +++ b/docs/docs/quality-gates.md @@ -15,7 +15,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2.275 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **196 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. +- O arnês inteiro são **197 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. - Quantas invariantes rodam como **críticas** numa execução **não é derivável do fonte**: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do `KNOWN-BUGS.md`, atualizado com saída real. Reproduza: diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 9402aca2c..bff499a2c 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -51,7 +51,7 @@ this page was aging at the very first commit — see | Playable characters | 44, in 5 factions | `CHARACTERS` array in `characters.js` | | Maps in the registry | 12 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 196 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 197 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Version | `2.0.0-alpha.172` | `public/js/version.js` and `package.json` (match) | @@ -277,10 +277,10 @@ And the two gates, with the exact list of what each one runs — straight from ` {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam +npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:autofix eval:preload eval:docsautoria eval:replaycam ``` -`package.json` has **120 scripts**; the reason behind each one lives in `SCRIPTS.md`. +`package.json` has **121 scripts**; the reason behind each one lives in `SCRIPTS.md`. > Block generated by `node tools/gen-docs.mjs`. Source: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md index 52444e462..7673a7059 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md @@ -18,7 +18,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2,275 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **196 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. +- The harness contains **197 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. - The number of critical checks in one run depends on the inputs present on that machine; dated results belong in `KNOWN-BUGS.md`. ```bash diff --git a/package.json b/package.json index 9d6c196b8..a086ed65f 100644 --- a/package.json +++ b/package.json @@ -118,12 +118,13 @@ "eval:regen": "node tools/eval/regen-check.mjs", "eval:ctfwin": "node tools/eval/ctf-win-check.mjs", "eval:spawn": "node tools/eval/spawn-settle-check.mjs", - "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam", - "check:fast": "node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:preload eval:docsautoria eval:replaycam", + "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:autofix eval:preload eval:docsautoria eval:replaycam", + "check:fast": "node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:wfsecret eval:deploygate eval:autofix eval:preload eval:docsautoria eval:replaycam", "eval:tags": "node tools/eval/tag-main-check.mjs", "eval:simclock": "node tools/eval/sim-clock-check.mjs", "eval:wfsecret": "node tools/eval/workflow-secret-check.mjs", - "eval:deploygate": "node tools/eval/deploy-gate-check.mjs" + "eval:deploygate": "node tools/eval/deploy-gate-check.mjs", + "eval:autofix": "node tools/eval/autofix-check.mjs" }, "repository": { "type": "git", diff --git a/scripts/ci/autofix_allowlist.py b/scripts/ci/autofix_allowlist.py new file mode 100644 index 000000000..99a271143 --- /dev/null +++ b/scripts/ci/autofix_allowlist.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""A trava do autofix: decide se o que o bot mexeu pode virar commit. + +O bot só pode tocar em ARQUIVO GERADO — o que uma ferramenta do repositório +reescreve inteiro a partir do código. Se o conserto encostar em qualquer outro +caminho, o run aborta e comenta em vez de commitar. + +A regra não é conveniência: sem ela, o primeiro conserto errado reescreve o mapa +de um colaborador e ninguém repara. Com ela, o pior caso do bot é regenerar uma +doc que já era derivada. + +Uso: git status --porcelain | python3 scripts/ci/autofix_allowlist.py + python3 scripts/ci/autofix_allowlist.py --selftest +""" +import sys + +# Caminhos que uma ferramenta REGERA por inteiro. Crescer esta lista é decisão de +# dono: cada entrada nova é um arquivo que o bot passa a poder sobrescrever sozinho. +PERMITIDOS = ( + "ARCH.generated.md", + "README.md", + "STATUS.md", + "SCRIPTS.md", + "docs/", + "tools/eval/ARCH.md", +) + +# Caminhos que NUNCA entram, mesmo que um prefixo permitido pareça cobri-los. O +# `.github/` fica de fora porque um bot que edita o próprio workflow que o governa +# consegue ampliar a própria permissão em um commit. +PROIBIDOS = ( + ".github/", + "scripts/", + "supabase/", + "package.json", + "package-lock.json", + "vercel.json", +) + + +def permitido(caminho: str) -> bool: + if any(caminho.startswith(p) for p in PROIBIDOS): + return False + return any(caminho == p or caminho.startswith(p) for p in PERMITIDOS) + + +def separa(caminhos: list[str]) -> tuple[list[str], list[str]]: + ok = [c for c in caminhos if permitido(c)] + fora = [c for c in caminhos if not permitido(c)] + return ok, fora + + +def caminhos_do_porcelain(texto: str) -> list[str]: + """`git status --porcelain` -> lista de caminhos. + + Renomeio vem como `R velho -> novo`; o que importa é o destino, senão um + rename para fora da lista passaria como se fosse o arquivo de origem. + """ + saida = [] + for linha in texto.splitlines(): + if len(linha) < 4: + continue + caminho = linha[3:].strip() + if " -> " in caminho: + caminho = caminho.split(" -> ", 1)[1] + saida.append(caminho.strip('"')) + return saida + + +def selftest() -> int: + casos = [ + ("doc gerada passa", ["ARCH.generated.md"], True), + ("pasta docs passa", ["docs/docs/comecando.md"], True), + ("código do jogo NÃO passa", ["public/js/main.js"], False), + ("mapa de colaborador NÃO passa", ["public/js/map_havan.js"], False), + ("workflow NÃO passa", [".github/workflows/ci.yml"], False), + ("script de CI NÃO passa", ["scripts/ci/pr_classify.py"], False), + ("package.json NÃO passa", ["package.json"], False), + ("uma proibida contamina o lote", ["STATUS.md", "public/js/game.js"], False), + ("nada mexido é lote vazio", [], True), + ] + erros = 0 + for nome, caminhos, esperado in casos: + _, fora = separa(caminhos) + obtido = not fora + ok = obtido == esperado + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} {nome}: fora={fora!r}") + + porcelain = ' M ARCH.generated.md\nR docs/a.md -> public/js/game.js\n?? novo.txt\n' + lidos = caminhos_do_porcelain(porcelain) + ok = lidos == ["ARCH.generated.md", "public/js/game.js", "novo.txt"] + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} renomeio é julgado pelo DESTINO: {lidos!r}") + + print("selftest verde" if not erros else f"{erros} caso(s) vermelho(s)") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + caminhos = caminhos_do_porcelain(sys.stdin.read()) + ok, fora = separa(caminhos) + if fora: + print("BLOQUEADO: o conserto encostou em arquivo que o bot não pode reescrever:") + for c in fora: + print(f" - {c}") + return 1 + for c in ok: + print(c) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/ensure_labels.py b/scripts/ci/ensure_labels.py index e515c34ae..e54053b22 100644 --- a/scripts/ci/ensure_labels.py +++ b/scripts/ci/ensure_labels.py @@ -18,6 +18,7 @@ "stale-backlog": ("c2e0c6", "Issue antiga ou desalinhada com o checkout atual; precisa revalidação"), "needs-repro": ("f9d0c4", "Issue precisa passos de reprodução mais concretos"), "preview-autorizado": ("0e8a16", "Mantenedor revisou o SHA atual e liberou preview do fork na Vercel"), + "pronto-pra-merge": ("0e8a16", "Portões verdes e diff dentro do aceito: falta só o merge, que é humano"), } diff --git a/tools/eval/autofix-check.mjs b/tools/eval/autofix-check.mjs new file mode 100644 index 000000000..40b3f5522 --- /dev/null +++ b/tools/eval/autofix-check.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node +/* ============================================================================ + autofix-check.mjs — O BOT CONSERTA, MAS SÓ O QUE É DERIVADO + ---------------------------------------------------------------------------- + O autofix é a primeira máquina deste repositório com permissão de ESCREVER num + PR. Isso só é aceitável enquanto três coisas valerem, e esta régua é quem as + segura: + + AF1 · toda escrita passa pela lista de permissão. Um `git commit`/`git push` no + autofix.yml que não seja precedido pela trava é a régua inteira contornada. + AF2 · a lista nunca cobre caminho de código. `public/`, `src/`, `scripts/`, + `.github/` e os manifestos ficam de fora — inclusive `.github/`, porque um + bot que edita o workflow que o governa amplia a própria permissão. + AF3 · o bot não mergeia. Deixar o PR pronto e fechá-lo são coisas diferentes, e + a segunda é humana (decisão do dono, 21/08). + AF4 · e isso vale para o repositório INTEIRO, não só para o autofix: nenhum + workflow pode chamar `gh pr merge`. O `csbrasil-bot-automerge` chamava, e + era a única coisa que um bot daqui fazia sozinho — justamente a que não + devia. Ele agora aplica `pronto-pra-merge` e o botão continua humano. + + Uso: node tools/eval/autofix-check.mjs [--mutante=] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { + 'autofix-sem-trava': 'AF1', + 'lista-abre-codigo': 'AF2', + 'lista-abre-workflow': 'AF2', + 'autofix-mergeia': 'AF3', + 'automerge-volta': 'AF4', +}; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const ler = (p) => { try { return readFileSync(p, 'utf8'); } catch { return ''; } }; +const falhas = []; + +let wf = ler('.github/workflows/autofix.yml'); +let lista = ler('scripts/ci/autofix_allowlist.py'); + +if (MUT === 'autofix-sem-trava') wf = wf.replace(/autofix_allowlist\.py/g, 'cat'); +if (MUT === 'autofix-mergeia') wf = wf.replace('git push origin "HEAD:$HEAD_REF"', 'git push origin "HEAD:$HEAD_REF"\n gh pr merge "$PR_NUMBER" --squash --auto'); +if (MUT === 'lista-abre-codigo') lista = lista.replace(' "docs/",', ' "docs/",\n "public/",'); +if (MUT === 'lista-abre-workflow') lista = lista.replace(/^\s*"\.github\/",$/m, ''); + +if (!wf) { console.log(' \x1b[31m✗\x1b[0m AF0 .github/workflows/autofix.yml não existe'); process.exit(1); } + +/* ---- AF1: escrita só depois da trava ---- */ +const usaTrava = /autofix_allowlist\.py/.test(wf); +const escreve = /git (commit|push)/.test(wf); +const posTrava = wf.indexOf('autofix_allowlist.py'); +const posCommit = wf.search(/git commit/); +if (escreve && (!usaTrava || posCommit < posTrava)) { + falhas.push('AF1 autofix.yml escreve no PR sem passar pela lista de permissão antes'); +} +/* O commit tem de usar SÓ o que a trava liberou, não um `git add -A`. */ +if (/git add\s+(-A|--all|\.)(\s|$)/.test(wf)) { + falhas.push('AF1 autofix.yml usa `git add` abrangente — o commit deve sair da saída da trava'); +} + +/* ---- AF2: a lista não cobre código ---- */ +if (!lista) falhas.push('AF2 scripts/ci/autofix_allowlist.py não existe'); +else { + const bloco = (lista.match(/PERMITIDOS = \(([\s\S]*?)\)/) || [])[1] || ''; + const proibidos = (lista.match(/PROIBIDOS = \(([\s\S]*?)\)/) || [])[1] || ''; + for (const perigoso of ['public/', 'src/', 'supabase/', 'scripts/', '.github/']) { + if (new RegExp(`"${perigoso.replace('.', '\\.')}`).test(bloco)) { + falhas.push(`AF2 a lista de permissão cobre \`${perigoso}\` — o bot passa a poder reescrever código`); + } + } + for (const obrigatorio of ['.github/', 'scripts/', 'package.json']) { + if (!new RegExp(`"${obrigatorio.replace('.', '\\.')}`).test(proibidos)) { + falhas.push(`AF2 \`${obrigatorio}\` não está na lista de proibidos`); + } + } +} + +/* ---- AF3: o bot não mergeia ---- */ +if (/gh pr merge/.test(wf)) { + falhas.push('AF3 autofix.yml mergeia PR — o autofix deixa pronto, quem fecha é gente'); +} + +/* ---- AF4: e nenhum outro workflow mergeia tampouco ---- */ +const WORKFLOWS = readdirSync('.github/workflows').filter((f) => /\.ya?ml$/.test(f)); +for (const nome of WORKFLOWS) { + let texto = ler(`.github/workflows/${nome}`); + if (MUT === 'automerge-volta' && nome === 'csbrasil-bot-automerge.yml') { + texto += "\n subprocess.run(['gh', 'pr', 'merge', pr, '--squash', '--auto'])\n"; + } + /* Só a CHAMADA conta: o comentário que explica por que ela saiu tem de poder citá-la. */ + const linhas = texto.split('\n').filter((l) => !/^\s*#/.test(l)); + if (linhas.some((l) => /gh['"]?,\s*['"]pr['"],\s*['"]merge['"]|gh pr merge/.test(l))) { + falhas.push(`AF4 ${nome} mergeia PR sozinho — deixar pronto e fechar são coisas diferentes`); + } +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(' \x1b[32m✓\x1b[0m AF autofix escreve só o que a trava libera, a lista não cobre código e o bot não mergeia'); +if (MUT && !falhas.length) console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego`); +process.exit(falhas.length ? 1 : 0);