From 18a5af3512983df77ee81fd4d264c54268a57546 Mon Sep 17 00:00:00 2001 From: vinimartinsufrr Date: Tue, 29 Sep 2026 09:19:32 -0400 Subject: [PATCH] =?UTF-8?q?fix(ci):=20check=20obrigat=C3=B3rio=20passa=20s?= =?UTF-8?q?em=20rodar=20(needs,=20continue-on-error,=20matrix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_checks_obrigatorios (scripts/qa/ci_invariants.sh) garantia que um check obrigatório existe e não tem `if:` no job, mas não pegava três outros jeitos de ele reportar sucesso sem ter rodado nada: - needs: apontando para um job fora de CHECKS_OBRIGATORIOS (ex.: android-e2e) — se esse job falhar, o obrigatório fica skipped, e skipped conta como aprovado na proteção de branch. - continue-on-error: true no job, que transforma falha em sucesso. - strategy.matrix, que muda o nome do check para "job (valor)" e não casa mais com o contexto exigido pela proteção — a PR espera para sempre. Também corrige a mensagem do caso se `if:` já coberto: dizia "pode nunca reportar", mas um job pulado por if: sempre reporta, só que reporta skipped/ verde — o bloqueio já estava certo, só a causa citada estava errada. scripts/qa/ci_invariants_test.sh cobre as três checagens novas com fixture positiva e negativa cada (ex.: needs: para outro job obrigatório não deve disparar, só needs: para um job fora do conjunto obrigatório deve), plugado no job workflow-lint logo depois do ci_invariants.sh. De passagem, força PYTHONIOENCODING=utf-8 na chamada do Python — sem isso as mensagens de falha saem corrompidas em locales que não são UTF-8 por padrão (achado ao rodar localmente neste Windows; sem efeito nos runners Linux do CI, que já usam UTF-8, mas deixa o script correto independente do host). CLAUDE.md e AGENTS.md atualizados no mesmo commit com as novas condições de falha e a lista de lacunas conhecidas (issues #19-#24), por exigência do próprio workflow-lint. Closes #20 Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 1 + AGENTS.md | 4 +- CLAUDE.md | 4 +- scripts/qa/ci_invariants.sh | 52 +++++++++- scripts/qa/ci_invariants_test.sh | 163 +++++++++++++++++++++++++++++++ 5 files changed, 216 insertions(+), 8 deletions(-) create mode 100755 scripts/qa/ci_invariants_test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3787b7b..84995fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: run: | python3 -c 'import yaml' 2>/dev/null || { sudo apt-get update && sudo apt-get install -y python3-yaml; } ./scripts/qa/ci_invariants.sh + ./scripts/qa/ci_invariants_test.sh serverpod-backend: runs-on: ubuntu-24.04 defaults: diff --git a/AGENTS.md b/AGENTS.md index 4b860ec..9085ffd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,10 +145,10 @@ Importante: - os testes de integração e validação de conexão vivem fora do `flutter test` e utilizam a stack Docker/VM; - a validação contra a stack real está na skill `validacao-e2e` (`scripts/qa/e2e.sh`, `tool/live_check.dart`, `integration_test` no emulador); - o CI ([.github/workflows/ci.yml](.github/workflows/ci.yml)) roda em toda PR, em push para `main`/`develop` e à mão (`gh workflow run CI --ref `), e tem nove jobs: `workflow-lint`, `serverpod-backend`, `backend-docker-build`, `patient-app`, `acs-app`, `admin-app`, `coverage-report`, `android-e2e` (único que sobe emulador Android contra a stack) e `admin-android-build` (compila o APK do admin); -- o `workflow-lint` roda actionlint e `scripts/qa/ci_invariants.sh`, que falha se a lista de jobs divergir de `JOBS_DOCUMENTADOS`, se um job sair do runner fixado (`ubuntu-24.04`, nunca `ubuntu-latest`), se uma ação cair abaixo da major em node24 (`checkout@v7`, `setup-java@v6`, `cache@v6`, `upload-artifact@v7`), se o workflow ganhar filtro de `paths` ou perder o grupo de `concurrency` por PR/SHA, se o `android-e2e` perder a limpeza de `pg_data/` ou se a chave do cache de AVD não terminar em `-`; +- o `workflow-lint` roda actionlint e `scripts/qa/ci_invariants.sh`, que falha se a lista de jobs divergir de `JOBS_DOCUMENTADOS`, se um job sair do runner fixado (`ubuntu-24.04`, nunca `ubuntu-latest`), se uma ação cair abaixo da major em node24 (`checkout@v7`, `setup-java@v6`, `cache@v6`, `upload-artifact@v7`), se o workflow ganhar filtro de `paths` ou perder o grupo de `concurrency` por PR/SHA, se o `android-e2e` perder a limpeza de `pg_data/`, se a chave do cache de AVD não terminar em `-`, ou se um check obrigatório tiver `if:`, `continue-on-error: true`, `strategy.matrix` ou `needs:` apontando para fora do conjunto obrigatório (issue #20 — qualquer um desses faz o job reportar `skipped`/verde sem ter rodado, e a proteção de branch conta isso como aprovado). Logo depois roda `scripts/qa/ci_invariants_test.sh`, com fixture positiva e negativa para cada checagem nova, pra essas checagens não ficarem sem teste nenhum; - `main` e `develop` são protegidas: os 8 checks de `./scripts/qa/ci_invariants.sh --checks-obrigatorios` (todo job exceto `android-e2e`) precisam passar para mesclar, e `main` exige PR; admins ainda podem dar push direto. O `android-e2e` está verde desde as correções de 2026-09-28, mas segue informativo até acumular histórico; - o script não lê a proteção configurada no GitHub: ao renomear ou criar um job, reaplique-a (ver `CONTRIBUTING.md` › CI e merge), senão as PRs ficam esperando um check que não existe mais; -- migrar para o Ubuntu 26.04 (`ubuntu-latest` migra em 2026-10-19; um ensaio passou 9/9) é uma PR que troca juntos `runs-on`, `RUNNER` e o sufixo da chave do AVD, e precisa de um actionlint que conheça o rótulo `ubuntu-26.04`. Histórico em [docs/ci-audit/2026-09-28-avaliacao-ci-develop.md](docs/ci-audit/2026-09-28-avaliacao-ci-develop.md); lacunas conhecidas da guarda nas issues #19 a #24. +- migrar para o Ubuntu 26.04 (`ubuntu-latest` migra em 2026-10-19; um ensaio passou 9/9) é uma PR que troca juntos `runs-on`, `RUNNER` e o sufixo da chave do AVD, e precisa de um actionlint que conheça o rótulo `ubuntu-26.04`. Histórico em [docs/ci-audit/2026-09-28-avaliacao-ci-develop.md](docs/ci-audit/2026-09-28-avaliacao-ci-develop.md); lacunas conhecidas da guarda nas issues #19, #21 a #24 (#20 corrigida — ver `check_checks_obrigatorios` e `scripts/qa/ci_invariants_test.sh`). ## Observações finais diff --git a/CLAUDE.md b/CLAUDE.md index f58b0f6..e26f1c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,8 +105,8 @@ Product/architecture source of truth (PRD, UX flows, LGPD design, stack decision - Keep triage/prioritization logic deterministic and consistent with the Manchester Protocol model referenced in the PRD — do not make risk classification probabilistic or user-overridable. - When touching sync behavior (backend `SyncFsm` or the ACS `offline_visit_queue.dart`), preserve retry/queue/conflict semantics — offline-first correctness is the primary architectural risk called out in `AGENTS.md`. - When reusing a clinical fill color (`red`/`accent`/`danger`/`yellow`/`green`) as text or icon color in the Flutter apps, use the `*OnSurface` token and measure contrast against the surface it actually renders on (commonly `Card`/`surfaceRaised`), not the Scaffold background — see the WCAG contrast tokens section in [apps/CLAUDE.md](apps/CLAUDE.md), `spec/ux_accessibility_assessment.md` and each app's `test/contrast_tokens_test.dart`. -- CI lives in [.github/workflows/ci.yml](.github/workflows/ci.yml) and runs on every PR, on pushes to `main`/`develop`, and by hand (`gh workflow run CI --ref `). 9 jobs: `workflow-lint`, `serverpod-backend`, `backend-docker-build`, `patient-app`, `acs-app`, `admin-app`, `coverage-report`, `android-e2e` (the only one that boots a real emulator against the stack), `admin-android-build`. `workflow-lint` runs actionlint plus `scripts/qa/ci_invariants.sh`, which fails when: this job list drifts from `JOBS_DOCUMENTADOS`; a job leaves the pinned runner (`RUNNER = 'ubuntu-24.04'`, never `ubuntu-latest`); an action drops below its node24 major (`checkout@v7`, `setup-java@v6`, `cache@v6`, `upload-artifact@v7`); the workflow gains a `paths` filter or loses the per-PR/per-SHA `concurrency` group; `android-e2e` loses its `pg_data/` cleanup step; or the AVD cache key does not end in `-`. -- `main` and `develop` are protected: the 8 checks from `./scripts/qa/ci_invariants.sh --checks-obrigatorios` (every job except `android-e2e`, tied to GitHub Actions app 15368) must pass to merge, and `main` also requires a PR; admins can still push directly. `android-e2e` has been green since the fixes of 2026-09-28 but stays informational until it builds a longer history. The script does not read the live protection: after renaming or adding a job, re-apply it (see `CONTRIBUTING.md` › CI e merge) or PRs wait forever for a check that no longer exists. Moving to Ubuntu 26.04 (`ubuntu-latest` migrates on 2026-10-19; a rehearsal ran 9/9 green) is a PR that changes `runs-on`, `RUNNER` and the AVD key suffix together, and needs an actionlint that knows the `ubuntu-26.04` label. History in `docs/ci-audit/2026-09-28-avaliacao-ci-develop.md`; known gaps in the guard are issues #19–#24. +- CI lives in [.github/workflows/ci.yml](.github/workflows/ci.yml) and runs on every PR, on pushes to `main`/`develop`, and by hand (`gh workflow run CI --ref `). 9 jobs: `workflow-lint`, `serverpod-backend`, `backend-docker-build`, `patient-app`, `acs-app`, `admin-app`, `coverage-report`, `android-e2e` (the only one that boots a real emulator against the stack), `admin-android-build`. `workflow-lint` runs actionlint plus `scripts/qa/ci_invariants.sh`, which fails when: this job list drifts from `JOBS_DOCUMENTADOS`; a job leaves the pinned runner (`RUNNER = 'ubuntu-24.04'`, never `ubuntu-latest`); an action drops below its node24 major (`checkout@v7`, `setup-java@v6`, `cache@v6`, `upload-artifact@v7`); the workflow gains a `paths` filter or loses the per-PR/per-SHA `concurrency` group; `android-e2e` loses its `pg_data/` cleanup step; the AVD cache key does not end in `-`; or a required check has `if:`, `continue-on-error: true`, `strategy.matrix`, or a `needs:` pointing outside the required set (issue #20 — each of those makes a required job report `skipped`/green without actually running, which branch protection counts as approval). `scripts/qa/ci_invariants_test.sh` runs right after it, with a positive and negative fixture per new case, so those checks don't drift silently either. +- `main` and `develop` are protected: the 8 checks from `./scripts/qa/ci_invariants.sh --checks-obrigatorios` (every job except `android-e2e`, tied to GitHub Actions app 15368) must pass to merge, and `main` also requires a PR; admins can still push directly. `android-e2e` has been green since the fixes of 2026-09-28 but stays informational until it builds a longer history. The script does not read the live protection: after renaming or adding a job, re-apply it (see `CONTRIBUTING.md` › CI e merge) or PRs wait forever for a check that no longer exists. Moving to Ubuntu 26.04 (`ubuntu-latest` migrates on 2026-10-19; a rehearsal ran 9/9 green) is a PR that changes `runs-on`, `RUNNER` and the AVD key suffix together, and needs an actionlint that knows the `ubuntu-26.04` label. History in `docs/ci-audit/2026-09-28-avaliacao-ci-develop.md`; known gaps in the guard are issues #19, #21–#24 (#20 fixed — see `check_checks_obrigatorios` and `scripts/qa/ci_invariants_test.sh`). - Never commit real patient data, credentials, or the dev Docker Compose secrets into anything beyond local development. ## graphify diff --git a/scripts/qa/ci_invariants.sh b/scripts/qa/ci_invariants.sh index 9e3fb82..35c6552 100755 --- a/scripts/qa/ci_invariants.sh +++ b/scripts/qa/ci_invariants.sh @@ -13,11 +13,22 @@ # # Não precisa de rede nem da stack; só python3 com PyYAML. Roda no job # workflow-lint do próprio CI. +# +# CI_INVARIANTS_WORKFLOW aponta para um workflow diferente do real — só para +# scripts/qa/ci_invariants_test.sh testar as checagens contra fixtures +# sintéticas, sem tocar em .github/workflows/ci.yml. set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +caminho_workflow="${CI_INVARIANTS_WORKFLOW:-$repo_root/.github/workflows/ci.yml}" + +# Sem isto, o Python usa a codificação do locale do host para stdout/stderr — +# em runners Linux normalmente já é UTF-8, mas não é garantido (e não é, por +# padrão, no Windows), e as mensagens de falha têm acento. Sem UTF-8 forçado, +# elas saem corrompidas em vez de crashar, o que é pior: passa despercebido. +export PYTHONIOENCODING=utf-8 -exec python3 - "$repo_root/.github/workflows/ci.yml" "$@" <<'PY' +exec python3 - "$caminho_workflow" "$@" <<'PY' import json import sys @@ -165,6 +176,16 @@ CHECKS_OBRIGATORIOS = sorted(JOBS_DOCUMENTADOS - {'android-e2e'}) # publique um status com o mesmo nome e destrave o merge. APP_GITHUB_ACTIONS = 15368 +def _lista_needs(needs): + # `needs:` aceita string, lista ou (sem chave) None — normaliza pra uma + # lista só, sempre. + if needs is None: + return [] + if isinstance(needs, str): + return [needs] + return list(needs) + + def check_checks_obrigatorios(): if 'android-e2e' in CHECKS_OBRIGATORIOS: falhas.append('FINDING-4: android-e2e não pode ser obrigatório enquanto for instável') @@ -173,12 +194,35 @@ def check_checks_obrigatorios(): if job is None: falhas.append(f'FINDING-5: check obrigatório {nome} não existe no workflow') continue - # O nome do check é o `name:` do job, se houver; e um job com `if:` - # pode não rodar e nunca reportar. + # O nome do check é o `name:` do job, se houver. if job.get('name', nome) != nome: falhas.append(f"FINDING-5: {nome} tem name: {job['name']!r}; o check obrigatório não casaria") if 'if' in job: - falhas.append(f'FINDING-5: {nome} tem if: no nível do job; pode nunca reportar') + # Um job pulado pelo próprio `if:` reporta `skipped` — e a + # proteção de branch conta `skipped` como aprovado. Não é "pode + # nunca reportar": ele sempre reporta, só que reporta verde do + # jeito errado, e o merge destrava sem o job ter rodado nada + # (issue #20 corrige esta mensagem, que dizia o contrário). + falhas.append( + f'FINDING-5: {nome} tem if: no nível do job; skipped conta como aprovado e destrava o merge sem rodar' + ) + # issue #20: três outros jeitos de um check obrigatório passar verde + # sem ter, de fato, rodado. + if job.get('continue-on-error') is True: + falhas.append( + f'{nome} (obrigatório) tem continue-on-error: true; uma falha do job reporta sucesso' + ) + if 'matrix' in (job.get('strategy') or {}): + falhas.append( + f'{nome} (obrigatório) tem strategy.matrix; o nome do check vira "{nome} (valor)" ' + 'e não casa com o contexto exigido pela proteção — a PR espera para sempre' + ) + for dep in _lista_needs(job.get('needs')): + if dep not in CHECKS_OBRIGATORIOS: + falhas.append( + f'{nome} (obrigatório) tem needs: {dep}, que não é obrigatório; se {dep} falhar, ' + f'{nome} fica skipped e conta como aprovado' + ) CHECKS = [check_jobs, check_gatilhos, check_sem_filtro_de_paths, check_concorrencia, diff --git a/scripts/qa/ci_invariants_test.sh b/scripts/qa/ci_invariants_test.sh new file mode 100755 index 0000000..128ec95 --- /dev/null +++ b/scripts/qa/ci_invariants_test.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# +# Controles positivo e negativo das três checagens que a issue #20 pediu para +# scripts/qa/ci_invariants.sh: um check obrigatório que passa verde sem ter +# rodado por causa de continue-on-error, strategy.matrix ou needs: apontando +# para um job fora de CHECKS_OBRIGATORIOS. +# +# Por que existe: sem fixture sintética, as três checagens só seriam +# exercitadas contra o .github/workflows/ci.yml real do momento — que, por +# não ter nenhum dos três defeitos, nunca provaria que a checagem DETECTA o +# que promete detectar, só que ela não acende à toa no arquivo de hoje. Cada +# caso aqui tem o par positivo (o defeito existe, a mensagem tem de aparecer) +# e negativo (uma variação vizinha SEM o defeito, a mensagem não pode +# aparecer) — o mesmo padrão de scripts/qa/tls_invariants.sh. +# +# ./scripts/qa/ci_invariants_test.sh +# +# Não precisa de rede nem da stack; só python3 com PyYAML (mesma dependência +# do ci_invariants.sh, que este script chama via CI_INVARIANTS_WORKFLOW para +# apontar cada fixture em vez do .github/workflows/ci.yml real). +set -uo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +falhas=0 +ok() { echo " ok — $1"; } +falha() { echo " FALHA — $1" >&2; falhas=$((falhas + 1)); } + +# Roda o ci_invariants.sh contra a fixture $1 e devolve o stderr em $saida. +# Não usa `set -e`: um exit 1 é o resultado esperado sempre que a fixture tem +# um defeito de propósito. +rodar() { + local fixture="$1" + saida="$(CI_INVARIANTS_WORKFLOW="$fixture" "$repo_root/scripts/qa/ci_invariants.sh" 2>&1 1>/dev/null)" +} + +# Espera que $trecho apareça na saída da fixture $1. +espera_falha() { + local descricao="$1" fixture="$2" trecho="$3" + rodar "$fixture" + if printf '%s' "$saida" | grep -qF -- "$trecho"; then + ok "$descricao: a checagem acusa (\"$trecho\")" + else + falha "$descricao: esperava \"$trecho\" na saída, não apareceu — saída: $saida" + fi +} + +# Espera que $trecho NÃO apareça na saída da fixture $1 — o controle +# negativo: prova que a checagem não dispara por acidente numa variação +# vizinha sem o defeito. +espera_ok() { + local descricao="$1" fixture="$2" trecho="$3" + rodar "$fixture" + if printf '%s' "$saida" | grep -qF -- "$trecho"; then + falha "$descricao: não devia acusar (\"$trecho\") e acusou — saída: $saida" + else + ok "$descricao: a checagem não dispara por acidente" + fi +} + +# Workflow mínimo que passa por TODAS as outras checagens do ci_invariants.sh +# (jobs documentados, gatilhos, concurrency, versões de ação, runner, limpeza +# do pg_data) — assim, o único jeito de uma fixture derivada daqui falhar por +# causa das checagens desta issue é ela mesma ter o defeito que existe para +# provar. $1, se dado, substitui a linha ` # EXTRA` dentro do job +# admin-app (indentação de 4 espaços já incluída no marcador). +workflow_base() { + local extra_admin_app="${1:-}" + cat < "$caminho" + printf '%s' "$caminho" +} + +echo '== Controle de base: a fixture limpa não deve acusar nada =' +base_fixture="$(escreve base '')" +rodar "$base_fixture" +if [[ -z "$saida" ]]; then + ok 'a fixture base passa por todas as outras checagens sem ruído' +else + falha "a fixture base deveria estar limpa e não está — saída: $saida" +fi + +echo '== continue-on-error: true num check obrigatório vira sucesso mesmo falhando ==' +espera_falha 'positivo' \ + "$(escreve continue_on_error_true ' continue-on-error: true')" \ + 'admin-app (obrigatório) tem continue-on-error: true' +espera_ok 'negativo (continue-on-error: false, valor explícito)' \ + "$(escreve continue_on_error_false ' continue-on-error: false')" \ + 'continue-on-error: true' + +echo '== strategy.matrix num check obrigatório muda o nome do check ==' +espera_falha 'positivo' \ + "$(escreve matrix ' strategy: + matrix: + flutter: ["3.44.8"]')" \ + 'admin-app (obrigatório) tem strategy.matrix' +espera_ok 'negativo (strategy sem matrix, só fail-fast)' \ + "$(escreve strategy_sem_matrix ' strategy: + fail-fast: true')" \ + 'strategy.matrix' + +echo '== needs: apontando para fora de CHECKS_OBRIGATORIOS esconde o job que falhou ==' +espera_falha 'positivo (needs: android-e2e, não obrigatório)' \ + "$(escreve needs_fora ' needs: android-e2e')" \ + 'admin-app (obrigatório) tem needs: android-e2e' +espera_ok 'negativo (needs: acs-app, também obrigatório)' \ + "$(escreve needs_dentro ' needs: acs-app')" \ + 'admin-app (obrigatório) tem needs:' + +echo +if [[ "$falhas" -eq 0 ]]; then + echo 'OK — as checagens de check obrigatório "verde sem rodar" (issue #20) conferem.' +else + echo "FALHOU — $falhas asserção(ões) não conferem." >&2 +fi +[[ "$falhas" -eq 0 ]]