Merge pull request #1978 from melgarafael/fix/hook-migration-populacao-em-arquivo

fix(hooks): população de migrations em arquivo — o hook não termina com 12 mil refs
This commit is contained in:
Rafael Melgaço
2026-10-01 11:51:11 -03:00
committed by GitHub
4 changed files with 93 additions and 21 deletions
@@ -125,16 +125,21 @@ fi
# `populacao=` ANTES do `if`: o hook roda com `set -u`, e uma variável nunca
# atribuída aborta o script inteiro — o hook saía com 1 SEM mensagem nenhuma,
# que é o pior formato de falha possível num guard.
populacao=""
# A população vai para um ARQUIVO, nunca para uma variável: no clone do mantenedor
# (29/09/2026) ela tinha ~1,29 M linhas (~112 MB), e o teste de vazio que havia aqui,
# `${populacao// /}`, é uma substituição de padrão do bash — quadrática no tamanho da
# string. O hook não terminava em 45 min. O grep lê o arquivo direto.
populacao="$(mktemp)" || { echo "pre-commit BLOQUEADO: mktemp falhou — a população de migrations não pôde ser medida. Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2; exit 1; }
trap 'rm -f "$populacao"' EXIT
if declare -F pop_migrations >/dev/null 2>&1; then
# O HEAD entra SEMPRE: `pop_refs_de_outrem` tira a ref cujo SHA é o do HEAD (a
# #1155), e sem devolvê-lo aqui a migration que a PRÓPRIA branch já commitou
# sumia da conta — a segunda 0411 e o carimbo repetido passavam calados, e a
# dica de próximo livre apontava para o número da branch. O próprio arquivo
# encenado não é acusado: o `grep -vE " <nome>$"` abaixo o tira.
populacao="$(pop_migrations $refs HEAD 2>/dev/null || true)"
{ pop_migrations $refs HEAD 2>/dev/null || true; } >"$populacao" || { echo "pre-commit BLOQUEADO: não foi possível gravar a população de migrations em $populacao. Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2; exit 1; }
fi
if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; then
if ! grep -q . "$populacao"; then
# A biblioteca não está (clone antigo, cópia da skill de versão anterior) ou
# não achou migration nenhuma. Mede o que a versão anterior media e DIZ.
if ! declare -F pop_migrations >/dev/null 2>&1; then
@@ -148,7 +153,7 @@ if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; then
# fosse o primeiro da ref.
[ -n "$arquivos" ] && fallback="${fallback}$(awk -v r="$ref" '{ print r, $0 }' <<<"$arquivos")"$'\n'
done
populacao="$fallback"
printf '%s' "$fallback" >"$populacao"
echo "pre-commit AVISO: scripts/migration-populacao.sh AUSENTE — unicidade de NNNN medida sobre ${refs//$'\n'/ } (a população da pergunta: main do produto ∪ PRs abertos). Quem mede a inteira: pnpm checar:colisao-de-migration (#1273)" >&2
elif [ -z "${base}" ]; then
echo "pre-commit AVISO: Nenhuma migration resolvida na população ($base e as refs do clone) — a unicidade de NNNN NÃO foi medida (#1273). Rode: pnpm checar:colisao-de-migration" >&2
@@ -170,13 +175,13 @@ while IFS= read -r caminho; do
# A âncora é a do NOME CANÔNICO e o `grep` roda sobre a LINHA INTEIRA ("<ref>
# <nome>"), para o dono poder ser nomeado: "já existe em <ref>" sem o ref é
# "tomada" apontando para o nada, que é a armadilha da #1155.
donos_n="$(grep -E "^[A-Za-z0-9_./-]+ [0-9]{14}_${nnnn}_.+\.sql$" <<<"$populacao" | grep -vE " ${nome}\$" || true)"
donos_t="$(grep -E "^[A-Za-z0-9_./-]+ ${ts}_[0-9]{4}_.+\.sql$" <<<"$populacao" | grep -vE " ${nome}\$" || true)"
donos_n="$(grep -E "^[A-Za-z0-9_./-]+ [0-9]{14}_${nnnn}_.+\.sql$" "$populacao" | grep -vE " ${nome}\$" || true)"
donos_t="$(grep -E "^[A-Za-z0-9_./-]+ ${ts}_[0-9]{4}_.+\.sql$" "$populacao" | grep -vE " ${nome}\$" || true)"
if [ -n "$donos_n" ]; then
onde="$(awk '{printf "%s(%s) ", $1, $2}' <<<"$donos_n" | sed 's/ $//')"
echo "pre-commit BLOQUEADO: NNNN=$nnnn de '$nome' já existe em: $onde" >&2
if declare -F pop_dica_proximo_livre >/dev/null 2>&1; then
pop_dica_proximo_livre "$nnnn" "$base" "$populacao" >&2
pop_dica_proximo_livre "$nnnn" "$base" "$(cut -d' ' -f2- "$populacao" | LC_ALL=C sort -u)" >&2
else
echo " Para o próximo número livre (main do produto ∪ PRs abertos): pnpm checar:colisao-de-migration" >&2
fi
@@ -125,16 +125,21 @@ fi
# `populacao=` ANTES do `if`: o hook roda com `set -u`, e uma variável nunca
# atribuída aborta o script inteiro — o hook saía com 1 SEM mensagem nenhuma,
# que é o pior formato de falha possível num guard.
populacao=""
# A população vai para um ARQUIVO, nunca para uma variável: no clone do mantenedor
# (29/09/2026) ela tinha ~1,29 M linhas (~112 MB), e o teste de vazio que havia aqui,
# `${populacao// /}`, é uma substituição de padrão do bash — quadrática no tamanho da
# string. O hook não terminava em 45 min. O grep lê o arquivo direto.
populacao="$(mktemp)" || { echo "pre-commit BLOQUEADO: mktemp falhou — a população de migrations não pôde ser medida. Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2; exit 1; }
trap 'rm -f "$populacao"' EXIT
if declare -F pop_migrations >/dev/null 2>&1; then
# O HEAD entra SEMPRE: `pop_refs_de_outrem` tira a ref cujo SHA é o do HEAD (a
# #1155), e sem devolvê-lo aqui a migration que a PRÓPRIA branch já commitou
# sumia da conta — a segunda 0411 e o carimbo repetido passavam calados, e a
# dica de próximo livre apontava para o número da branch. O próprio arquivo
# encenado não é acusado: o `grep -vE " <nome>$"` abaixo o tira.
populacao="$(pop_migrations $refs HEAD 2>/dev/null || true)"
{ pop_migrations $refs HEAD 2>/dev/null || true; } >"$populacao" || { echo "pre-commit BLOQUEADO: não foi possível gravar a população de migrations em $populacao. Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2; exit 1; }
fi
if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; then
if ! grep -q . "$populacao"; then
# A biblioteca não está (clone antigo, cópia da skill de versão anterior) ou
# não achou migration nenhuma. Mede o que a versão anterior media e DIZ.
if ! declare -F pop_migrations >/dev/null 2>&1; then
@@ -148,7 +153,7 @@ if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; then
# fosse o primeiro da ref.
[ -n "$arquivos" ] && fallback="${fallback}$(awk -v r="$ref" '{ print r, $0 }' <<<"$arquivos")"$'\n'
done
populacao="$fallback"
printf '%s' "$fallback" >"$populacao"
echo "pre-commit AVISO: scripts/migration-populacao.sh AUSENTE — unicidade de NNNN medida sobre ${refs//$'\n'/ } (a população da pergunta: main do produto ∪ PRs abertos). Quem mede a inteira: pnpm checar:colisao-de-migration (#1273)" >&2
elif [ -z "${base}" ]; then
echo "pre-commit AVISO: Nenhuma migration resolvida na população ($base e as refs do clone) — a unicidade de NNNN NÃO foi medida (#1273). Rode: pnpm checar:colisao-de-migration" >&2
@@ -170,13 +175,13 @@ while IFS= read -r caminho; do
# A âncora é a do NOME CANÔNICO e o `grep` roda sobre a LINHA INTEIRA ("<ref>
# <nome>"), para o dono poder ser nomeado: "já existe em <ref>" sem o ref é
# "tomada" apontando para o nada, que é a armadilha da #1155.
donos_n="$(grep -E "^[A-Za-z0-9_./-]+ [0-9]{14}_${nnnn}_.+\.sql$" <<<"$populacao" | grep -vE " ${nome}\$" || true)"
donos_t="$(grep -E "^[A-Za-z0-9_./-]+ ${ts}_[0-9]{4}_.+\.sql$" <<<"$populacao" | grep -vE " ${nome}\$" || true)"
donos_n="$(grep -E "^[A-Za-z0-9_./-]+ [0-9]{14}_${nnnn}_.+\.sql$" "$populacao" | grep -vE " ${nome}\$" || true)"
donos_t="$(grep -E "^[A-Za-z0-9_./-]+ ${ts}_[0-9]{4}_.+\.sql$" "$populacao" | grep -vE " ${nome}\$" || true)"
if [ -n "$donos_n" ]; then
onde="$(awk '{printf "%s(%s) ", $1, $2}' <<<"$donos_n" | sed 's/ $//')"
echo "pre-commit BLOQUEADO: NNNN=$nnnn de '$nome' já existe em: $onde" >&2
if declare -F pop_dica_proximo_livre >/dev/null 2>&1; then
pop_dica_proximo_livre "$nnnn" "$base" "$populacao" >&2
pop_dica_proximo_livre "$nnnn" "$base" "$(cut -d' ' -f2- "$populacao" | LC_ALL=C sort -u)" >&2
else
echo " Para o próximo número livre (main do produto ∪ PRs abertos): pnpm checar:colisao-de-migration" >&2
fi
+12 -7
View File
@@ -108,16 +108,21 @@ fi
# `populacao=` ANTES do `if`: o hook roda com `set -u`, e uma variável nunca
# atribuída aborta o script inteiro — o hook saía com 1 SEM mensagem nenhuma,
# que é o pior formato de falha possível num guard.
populacao=""
# A população vai para um ARQUIVO, nunca para uma variável: no clone do mantenedor
# (29/09/2026) ela tinha ~1,29 M linhas (~112 MB), e o teste de vazio que havia aqui,
# `${populacao// /}`, é uma substituição de padrão do bash — quadrática no tamanho da
# string. O hook não terminava em 45 min. O grep lê o arquivo direto.
populacao="$(mktemp)" || { echo "pre-commit BLOQUEADO: mktemp falhou — a população de migrations não pôde ser medida. Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2; exit 1; }
trap 'rm -f "$populacao"' EXIT
if declare -F pop_migrations >/dev/null 2>&1; then
# O HEAD entra SEMPRE: `pop_refs_de_outrem` tira a ref cujo SHA é o do HEAD (a
# #1155), e sem devolvê-lo aqui a migration que a PRÓPRIA branch já commitou
# sumia da conta — a segunda 0411 e o carimbo repetido passavam calados, e a
# dica de próximo livre apontava para o número da branch. O próprio arquivo
# encenado não é acusado: o `grep -vE " <nome>$"` abaixo o tira.
populacao="$(pop_migrations $refs HEAD 2>/dev/null || true)"
{ pop_migrations $refs HEAD 2>/dev/null || true; } >"$populacao" || { echo "pre-commit BLOQUEADO: não foi possível gravar a população de migrations em $populacao. Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2; exit 1; }
fi
if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; then
if ! grep -q . "$populacao"; then
if ! declare -F pop_migrations >/dev/null 2>&1; then
fallback=""
for ref in $refs HEAD; do
@@ -129,7 +134,7 @@ if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; then
# fosse o primeiro da ref.
[ -n "$arquivos" ] && fallback="${fallback}$(awk -v r="$ref" '{ print r, $0 }' <<<"$arquivos")"$'\n'
done
populacao="$fallback"
printf '%s' "$fallback" >"$populacao"
echo "pre-commit AVISO: scripts/migration-populacao.sh AUSENTE — NNNN medido sobre ${refs//$'\n'/ }. Quem mede a população inteira (main do produto ∪ PRs abertos): pnpm checar:colisao-de-migration (#1273)" >&2
elif [ -z "$base" ]; then
echo "pre-commit AVISO: nenhuma migration resolvida na população ($base e as refs do clone) — a unicidade de NNNN NÃO foi medida (#1273). Rode: pnpm checar:colisao-de-migration" >&2
@@ -145,12 +150,12 @@ while IFS= read -r path; do
exit 1
fi
# O MESMO arquivo nesta população é o PR de quem roda: não é colisão.
conflict=$(grep -E "^[A-Za-z0-9_./-]+ [0-9]{14}_${nnnn}_.+\.sql$" <<<"$populacao" \
conflict=$(grep -E "^[A-Za-z0-9_./-]+ [0-9]{14}_${nnnn}_.+\.sql$" "$populacao" \
| grep -vE " ${fname}\$" || true)
if [ -n "$conflict" ]; then
echo "pre-commit BLOQUEADO: sequência NNNN=$nnnn de '$fname' já existe em: $(awk '{printf "%s(%s) ", $1, $2}' <<<"$conflict" | sed 's/ $//')" >&2
if declare -F pop_dica_proximo_livre >/dev/null 2>&1; then
pop_dica_proximo_livre "$nnnn" "$base" "$populacao" >&2
pop_dica_proximo_livre "$nnnn" "$base" "$(cut -d' ' -f2- "$populacao" | LC_ALL=C sort -u)" >&2
else
echo "Para o próximo número livre (main do produto ∪ PRs abertos): pnpm checar:colisao-de-migration" >&2
fi
@@ -194,7 +199,7 @@ while IFS= read -r path; do
[ -z "$ts" ] && continue
grep -qw "$ts" <<<"$DIVIDA_DE_TIMESTAMP" && continue
conflict=$(grep -E "^[A-Za-z0-9_./-]+ ${ts}_[0-9]{4}_.+\.sql$" <<<"$populacao" \
conflict=$(grep -E "^[A-Za-z0-9_./-]+ ${ts}_[0-9]{4}_.+\.sql$" "$populacao" \
| grep -vE " ${fname}\$" || true)
if [ -n "$conflict" ]; then
echo "pre-commit BLOQUEADO: o TIMESTAMP $ts de '$fname' já existe em: $(awk '{printf "%s(%s) ", $1, $2}' <<<"$conflict" | sed 's/ $//')" >&2
@@ -976,5 +976,62 @@ tripla "$m" 20260910040000_0414_livre.sql
r=$(rodar "$m" check-migration-triple.sh)
assert_exit "$(exit_de "$r")" 0 "MIG-CONTROLE: e um número de fato livre passa"
# ── check-migration-triple.sh · população GRANDE termina (os dois hooks) ─────────
# Medido em 29/09/2026 no clone do mantenedor: ~5.788 refs de outrem → ~1,29 M linhas
# (~112 MB) de população, e o hook não terminava em 45 min. A causa era o teste de
# vazio `${populacao// /}`: a substituição de padrão do bash é quadrática no tamanho da
# string (medido aqui: 25 mil linhas → 18 s só nessa linha; 100 mil → ~400 s o hook).
# A população agora vive num ARQUIVO e o grep lê o arquivo. O fixture tem ~300 mil
# linhas (500 migrations × 600 refs para o mesmo commit — `pop_migrations` emite uma
# linha por ref) e o LIMITE é folgado para máquina lenta: o hook de antes estoura
# qualquer limite razoável aqui, o de agora leva ~1 s. O fixture foi dimensionado para o
# antigo estourar TAMBÉM em locale C (LANG vazio), onde ele termina em ~10 s com 100 mil
# linhas e o caso passaria sem vigiar; em UTF-8 o antigo é bem mais lento. Rodam os DOIS hooks: o do
# contribuidor tinha a mesma linha.
printf '\ncheck-migration-triple.sh — população grande termina (mantenedor e contribuidor)\n'
LIMITE_S=30
rodar_com_limite() { # $1 = clone, $2 = caminho do hook relativo ao clone
local d=$1 h=$2 pid t=0 rc out="$TMP/saida-com-limite"
( cd "$d" && exec bash "$h" ) >"$out" 2>&1 & pid=$!
while kill -0 "$pid" 2>/dev/null && [ "$t" -lt "$LIMITE_S" ]; do sleep 1; t=$((t+1)); done
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
printf 'não terminou em %ss\n__EXIT__estourou\n' "$LIMITE_S"; return
fi
wait "$pid"; rc=$?
printf '%s\n__EXIT__%s\n' "$(cat "$out")" "$rc"
}
g="$TMP/mig-grande"; mkdir -p "$g/supabase/migrations" "$g/scripts" "$g/loop/hooks" "$g/contrib"
git -C "$g" init -q -b main
cp "$RAIZ/scripts/migration-populacao.sh" "$g/scripts/"
cp "$HOOKS_ORIGEM/check-migration-triple.sh" "$g/loop/hooks/"
cp "$RAIZ/.agents/skills/deskcomm-contribuir/scripts/hooks/check-migration-triple.sh" "$g/contrib/"
printf -- '-- baseline\n' > "$g/supabase/baseline.sql"
printf '| base |\n' > "$g/supabase/migrations/MANIFEST.md"
commitar "$g" "base"
git -C "$g" checkout -q -b outra
for i in $(seq 1 500); do : > "$g/supabase/migrations/$(printf '2026010100%04d_%04d_m.sql' "$i" "$i")"; done
commitar "$g" "500 migrations"
c_outra=$(git -C "$g" rev-parse HEAD)
git -C "$g" checkout -q main
seq 1 600 | awk -v c="$c_outra" '{ printf "create refs/heads/r%05d %s\n", $1, c }' | git -C "$g" update-ref --stdin
linhas=$( cd "$g" && bash -c '. scripts/migration-populacao.sh; pop_migrations $(pop_refs_de_outrem "") HEAD | wc -l' | tr -d ' ')
if [ "${linhas:-0}" -ge 300000 ]; then ok "MIG-GRANDE: a população do fixture tem $linhas linhas (premissa do volume)"
else falha "MIG-GRANDE: população >= 300000 linhas" "veio ${linhas:-nada} — o caso não estressa o hook"; fi
tripla "$g" 20990101000000_9999_livre.sql
for h in loop/hooks/check-migration-triple.sh contrib/check-migration-triple.sh; do
r=$(rodar_com_limite "$g" "$h")
assert_exit "$(exit_de "$r")" 0 "MIG-GRANDE ($h): número livre passa em menos de ${LIMITE_S}s"
done
git -C "$g" rm -q --cached supabase/migrations/20990101000000_9999_livre.sql
rm -f "$g/supabase/migrations/20990101000000_9999_livre.sql"
tripla "$g" 20990101000001_0001_colide.sql
for h in loop/hooks/check-migration-triple.sh contrib/check-migration-triple.sh; do
r=$(rodar_com_limite "$g" "$h")
assert_exit "$(exit_de "$r")" 1 "MIG-GRANDE ($h): NNNN tomado segue BLOQUEADO em menos de ${LIMITE_S}s"
assert_contains "$(saida_de "$r")" "r00200(20260101000001_0001_m.sql)" "MIG-GRANDE ($h): e o dono é nomeado"
assert_contains "$(saida_de "$r")" "próximo livre 0501" "MIG-GRANDE ($h): e a dica mede o teto da MESMA população"
done
printf '\nhooks-nao-acusam-a-main: %s casos, %s falha(s)\n' "$casos" "$falhas"
[ "$falhas" -eq 0 ] || exit 1