mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
Merge pull request #1776 from webtecnica/fix/1273-proximo-nnnn-livre-mede-a-populacao-certa
fix(#1273): "próximo NNNN livre" passa a medir a POPULAÇÃO da pergunta
This commit is contained in:
@@ -92,8 +92,13 @@ bash .agents/skills/deskcomm-contribuir/scripts/armar-hooks.sh
|
||||
```
|
||||
|
||||
Três guardas, e só isso: `pre-commit` reprova migration nova sem apêndice no `baseline.sql` e sem
|
||||
linha no `MANIFEST.md` no mesmo commit, e número (`NNNN`) ou timestamp já usado na `origin/main`
|
||||
ou em branch local; `pre-push` reprova push na `main`; e os dois avisam (sem bloquear) quando o
|
||||
linha no `MANIFEST.md` no mesmo commit, e número (`NNNN`) ou timestamp já usado na **população da
|
||||
pergunta** — a main do PRODUTO (o remoto que aponta para `melgarafael/DeskcommCRM`, com qualquer
|
||||
nome; num fork, a sua `origin/main` é a main do fork e não vale) mais `refs/heads` e `refs/remotes`.
|
||||
O que essa população não cobre são os PRs abertos, e a mensagem do hook diz isso e aponta
|
||||
`pnpm checar:colisao-de-migration`, que mede também eles. O `NNNN` sai da posição do nome canônico
|
||||
(`^[0-9]{14}_([0-9]{4})_`), e a regra está em `migration-populacao.sh`, na pasta `scripts/` da raiz do repositório (issue #1273);
|
||||
`pre-push` reprova push na `main`; e os dois avisam (sem bloquear) quando o
|
||||
commit está assinado como `root@…` ou sem e-mail — trabalho assinado assim não aparece no perfil
|
||||
do GitHub de quem fez. O mantenedor roda hooks próprios (`loop/hooks`); o script recusa
|
||||
sobrescrevê-los.
|
||||
|
||||
@@ -54,7 +54,11 @@ A tripla é indivisível: arquivo em `supabase/migrations/`, apêndice **idempot
|
||||
`create or replace function`, `drop policy if exists` + `create policy`), linha no `MANIFEST.md`.
|
||||
O kit self-host aplica **só o baseline** — migration sem apêndice não existe para quem instalou.
|
||||
|
||||
- `NNNN` e timestamp únicos contra `origin/main` (o hook confere; o script também). Ao renumerar,
|
||||
- `NNNN` e timestamp únicos contra a POPULAÇÃO da pergunta: a main do PRODUTO (o remoto que
|
||||
aponta para `melgarafael/DeskcommCRM`, com qualquer nome — num fork, `origin/main` é a main do
|
||||
fork) mais `refs/heads` e `refs/remotes`; os PRs abertos saem por fora, declarados, e quem os
|
||||
mede é `pnpm checar:colisao-de-migration` (issue #1273). O `NNNN` sai da posição do nome
|
||||
canônico (`^[0-9]{14}_([0-9]{4})_`), nunca de um `_NNNN_` do slug. Ao renumerar,
|
||||
troque o timestamp junto: renumerar só o `NNNN` é o que fabrica colisão de timestamp.
|
||||
- Tabela tenant-aware: `organization_id uuid not null references organizations(id) on delete cascade`,
|
||||
`enable row level security`, policy `tenant_isolation_<tabela>_all` via `fn_user_org_ids()`, e a
|
||||
|
||||
@@ -5,21 +5,98 @@
|
||||
# 1. mudança em supabase/baseline.sql (apêndice idempotente — é o que o kit
|
||||
# self-host aplica; migration que não chega lá não chega em quem instalou)
|
||||
# 2. linha em supabase/migrations/MANIFEST.md
|
||||
# E o NNNN e o TIMESTAMP do nome novo não podem existir em origin/main nem em
|
||||
# branch local: colisão é o defeito nº 1 da triagem (renumerada 11 vezes desde
|
||||
# agosto de 2026), e quem cria migration copiando outra copia os dois.
|
||||
# E o NNNN e o TIMESTAMP do nome novo não podem existir na POPULAÇÃO que os
|
||||
# mediu: a main do PRODUTO (o remoto que aponta para melgarafael/DeskcommCRM, com
|
||||
# qualquer nome) mais as outras refs do clone. Colisão é o defeito nº 1 da
|
||||
# triagem (renumerada 11 vezes desde agosto de 2026), e quem cria migration
|
||||
# copiando outra copia os dois. A população — e o que ela DEIXA de fora — é a
|
||||
# mesma dos outros sete lugares (#1273), em `scripts/migration-populacao.sh`.
|
||||
#
|
||||
# Diferença para o hook do mantenedor (loop/hooks): compara contra origin/main
|
||||
# (um fork raramente tem outras branches locais) e não conhece a dívida de
|
||||
# timestamp da main, que é assunto do mantenedor.
|
||||
# Diferença para o hook do mantenedor (loop/hooks): este não conhece a dívida
|
||||
# de timestamp da main, que é assunto do mantenedor.
|
||||
# Bypass explícito (correção orientada pelo mantenedor): DESKCOMM_MIGRATION_EDIT=1
|
||||
set -euo pipefail
|
||||
|
||||
[ "${DESKCOMM_MIGRATION_EDIT:-0}" = "1" ] && exit 0
|
||||
|
||||
top="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
|
||||
novas="$(git diff --cached --name-status | awk '$1 == "A" && $2 ~ /^supabase\/migrations\/.*\.sql$/ { print $2 }')"
|
||||
# ── A POPULAÇÃO da unicidade (issue #1273) ──────────────────────────────────────
|
||||
#
|
||||
# A pergunta deste hook é "esse número já foi usado ou reservado?", e a resposta
|
||||
# honesta é sobre a main do PRODUTO (o remoto que aponta para
|
||||
# melgarafael/DeskcommCRM, com QUALQUER nome) mais as outras refs do clone. A
|
||||
# versão anterior media `origin/main` + `git branch` — e essas duas são o
|
||||
# recorte que a issue mediu como errado: num clone de fork, a `origin/main` é a
|
||||
# main DO FORK (que pode estar atrás da do principal, e o hook saía com 0
|
||||
# dizendo que o número estava livre), e `git branch` só via branches LOCAIS.
|
||||
# A cabeça de um PR aberto de fork vive em `refs/pull/N/head` no repositório
|
||||
# pai, e no clone do mantenedor o 0269 do #965 aparecia só como cópia em
|
||||
# `refs/remotes/origin/pr/965` — nenhuma das 188 branches locais tinha esse
|
||||
# número. A cópia em `refs/remotes/*/pr/N` fica de fora de propósito: é cópia
|
||||
# de cabeça, não PR aberto, e sobrevive ao fechamento (965 cópias contra 43 PRs
|
||||
# abertos, medido em 19/09/2026) — os PRs abertos entram pela LISTA do `gh`, no
|
||||
# `pnpm checar:colisao-de-migration`.
|
||||
#
|
||||
# `pop_tem_nnnn`/`pop_ts_em_uso` casam o NNNN e o timestamp na POSIÇÃO do nome
|
||||
# canônico, e não em qualquer "_NNNN_": é a âncora do #1269, e a variante
|
||||
# gulosa pegava o número do SLUG (`…_0326_relatorio_2024_anual.sql` virava
|
||||
# 2024) e perdia a duplicata real de 0326.
|
||||
#
|
||||
# A POPULAÇÃO é a mesma dos outros sete lugares, com a regra de degradação
|
||||
# declarada: a base é a main do PRODUTO quando algum remoto é o principal; sem
|
||||
# ele, é a `origin/main` que houver — e a saída DIZ qual das duas foi. As
|
||||
# branches LOCAIS e REMOTAS entram sempre (a de antes era só a local). O 0269 do
|
||||
# #965 continua FORA desta conta: ele morava só na cópia `pr/965`, que fica
|
||||
# excluída de propósito; quem o pega é o `pnpm checar:colisao-de-migration`.
|
||||
BIBLIOTECA="$top/scripts/migration-populacao.sh"
|
||||
if [ -r "$BIBLIOTECA" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$BIBLIOTECA" || true
|
||||
fi
|
||||
aviso_fork=0
|
||||
base=""
|
||||
if declare -F pop_main_do_produto >/dev/null 2>&1; then
|
||||
base="$(pop_main_do_produto || true)"
|
||||
[ -n "$base" ] || base=""
|
||||
if [ -n "$base" ] && ! git rev-parse -q --verify "${base}^{commit}" >/dev/null 2>&1; then
|
||||
base=""
|
||||
fi
|
||||
fi
|
||||
if [ -z "$base" ]; then
|
||||
# Sem o principal no clone (fork sem `upstream`, ou biblioteca ausente): a
|
||||
# base cai para a `origin/main`, e o hook sai DIZENDO que a população não é a
|
||||
# da pergunta completa. Encolher o universo em silêncio é o defeito da #1273.
|
||||
# Só avisa quando HÁ remoto de GitHub (um fork): fixture sem remoto nenhum só
|
||||
# tem o que tem. Sem a biblioteca, o aviso de ausência mais abaixo já diz.
|
||||
if declare -F pop_tem_remoto_de_github >/dev/null 2>&1 && pop_tem_remoto_de_github; then
|
||||
aviso_fork=1
|
||||
fi
|
||||
base="origin/main"
|
||||
git rev-parse -q --verify "${base}^{commit}" >/dev/null 2>&1 || base=""
|
||||
fi
|
||||
|
||||
# O que já está na main do PRODUTO não é novidade deste commit. Um `git merge
|
||||
# upstream/main` encena as migrations da main como `A` relativo à branch, e a
|
||||
# população alargada (refs/remotes/*) tem branches do principal com OUTRO
|
||||
# arquivo de mesmo NNNN (17 NNNN da main em 31 branches do upstream, medido em
|
||||
# 27/09/2026): sem este filtro o merge da main era BLOQUEADO mandando renumerar
|
||||
# arquivo que todo mundo já tem — a orientação impossível que o loop/hooks
|
||||
# registrou (18/09/2026). É o mesmo filtro dele, sobre a base do PRODUTO.
|
||||
if [ -n "$base" ]; then
|
||||
novas="$(while IFS= read -r p; do
|
||||
[ -z "$p" ] && continue
|
||||
git cat-file -e "$base:$p" 2>/dev/null || printf '%s\n' "$p"
|
||||
done <<<"$novas")"
|
||||
fi
|
||||
|
||||
[ -z "$novas" ] && exit 0
|
||||
|
||||
if [ "$aviso_fork" = 1 ]; then
|
||||
echo "pre-commit AVISO: NNNN/timestamp NÃO MEDIDOS contra a main do PRODUTO: nenhum remoto aponta para melgarafael/DeskcommCRM (a base é a origin/main do seu fork, que pode estar atrás). Corrija com: git remote add upstream https://github.com/melgarafael/DeskcommCRM.git && git fetch upstream (#1273)" >&2
|
||||
fi
|
||||
|
||||
staged="$(git diff --cached --name-only)"
|
||||
falhou=0
|
||||
|
||||
@@ -33,10 +110,50 @@ if ! grep -qx 'supabase/migrations/MANIFEST.md' <<<"$staged"; then
|
||||
falhou=1
|
||||
fi
|
||||
|
||||
# Refs contra as quais a unicidade é medida: origin/main (se existe) + branches locais.
|
||||
refs="$(git branch --format='%(refname:short)' 2>/dev/null || true)"
|
||||
if git rev-parse -q --verify origin/main >/dev/null 2>&1; then refs="origin/main
|
||||
$refs"; fi
|
||||
if declare -F pop_refs_de_outrem >/dev/null 2>&1; then
|
||||
refs="$(pop_refs_de_outrem "$base" 2>/dev/null || true)"
|
||||
else
|
||||
refs="origin/main $(git for-each-ref --format='%(refname)' refs/heads refs/remotes 2>/dev/null || true)"
|
||||
fi
|
||||
# A lista que entrou é a POPULAÇÃO que o grep vai casar. Ela NUNCA pode ficar
|
||||
# vazia por acidente: um `pop_migrations` que sai vazio (biblioteca ausente,
|
||||
# clone sem nenhuma migration, `git` fora do repositório) transformaria o
|
||||
# `grep` de baixo em "não há colisão" e o hook LIBERARIA o commit — um guard
|
||||
# que encolhe o universo em silêncio é exatamente o defeito que a #1273 corrige.
|
||||
# A degradação declarada é a de antes (medir a `origin/main` e as refs que houver)
|
||||
# com um aviso, nunca a população zerada.
|
||||
# `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=""
|
||||
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)"
|
||||
fi
|
||||
if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; 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
|
||||
fallback=""
|
||||
for ref in $refs HEAD; do
|
||||
[ -n "$ref" ] || continue
|
||||
arquivos="$(git ls-tree -r --name-only "$ref" -- supabase/migrations 2>/dev/null \
|
||||
| sed 's#^supabase/migrations/##' || true)"
|
||||
# Prefixa CADA linha com a ref: o grep de baixo casa "<ref> <nome>", e
|
||||
# prefixar só a 1ª deixava passar colisão com qualquer arquivo que não
|
||||
# fosse o primeiro da ref.
|
||||
[ -n "$arquivos" ] && fallback="${fallback}$(awk -v r="$ref" '{ print r, $0 }' <<<"$arquivos")"$'\n'
|
||||
done
|
||||
populacao="$fallback"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
while IFS= read -r caminho; do
|
||||
nome="$(basename "$caminho")"
|
||||
@@ -47,23 +164,31 @@ while IFS= read -r caminho; do
|
||||
falhou=1
|
||||
continue
|
||||
fi
|
||||
while IFS= read -r ref; do
|
||||
[ -z "$ref" ] && continue
|
||||
existentes="$(git ls-tree -r --name-only "$ref" -- supabase/migrations 2>/dev/null | sed 's#^supabase/migrations/##' || true)"
|
||||
colisao_n="$(grep -E "^[0-9]{14}_${nnnn}_.+\.sql$" <<<"$existentes" | grep -vx "$nome" || true)"
|
||||
colisao_t="$(grep -E "^${ts}_[0-9]{4}_.+\.sql$" <<<"$existentes" | grep -vx "$nome" || true)"
|
||||
if [ -n "$colisao_n" ]; then
|
||||
echo "pre-commit BLOQUEADO: NNNN=$nnnn de '$nome' já existe em '$ref': $colisao_n" >&2
|
||||
echo " Próximo livre: git ls-tree -r --name-only origin/main -- supabase/migrations | sed -E 's/.*_([0-9]{4})_.*/\\1/' | sort -n | tail -1" >&2
|
||||
echo " Troque o TIMESTAMP junto (date -u +%Y%m%d%H%M%S) — renumerar só o NNNN é o que fabrica colisão de timestamp." >&2
|
||||
falhou=1
|
||||
# O MESMO arquivo nesta população é o PR de quem roda (o índice ainda o
|
||||
# mostra como `A`): ele não é colisão, e acusar o autor de colidir com a
|
||||
# própria branch é a armadilha que a #1155 registrou.
|
||||
# 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)"
|
||||
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
|
||||
else
|
||||
echo " Para o próximo número livre (main do produto ∪ PRs abertos): pnpm checar:colisao-de-migration" >&2
|
||||
fi
|
||||
if [ -n "$colisao_t" ]; then
|
||||
echo "pre-commit BLOQUEADO: timestamp $ts de '$nome' já existe em '$ref': $colisao_t" >&2
|
||||
echo " O Supabase usa o timestamp como identidade da migration; dois iguais quebram db push/reset." >&2
|
||||
falhou=1
|
||||
fi
|
||||
done <<<"$refs"
|
||||
echo " Troque o TIMESTAMP junto (date -u +%Y%m%d%H%M%S) — renumerar só o NNNN é o que fabrica colisão de timestamp." >&2
|
||||
falhou=1
|
||||
fi
|
||||
if [ -n "$donos_t" ]; then
|
||||
onde="$(awk '{printf "%s(%s) ", $1, $2}' <<<"$donos_t" | sed 's/ $//')"
|
||||
echo "pre-commit BLOQUEADO: timestamp $ts de '$nome' já existe em: $onde" >&2
|
||||
echo " O Supabase usa o timestamp como identidade da migration; dois iguais quebram db push/reset." >&2
|
||||
falhou=1
|
||||
fi
|
||||
done <<<"$novas"
|
||||
|
||||
if [ "$falhou" = 1 ]; then
|
||||
|
||||
@@ -14,6 +14,13 @@ set -uo pipefail
|
||||
raiz="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "✗ fora de um clone git"; exit 0; }
|
||||
cd "$raiz"
|
||||
|
||||
# A POPULAÇÃO da unicidade de NNNN é a mesma dos hooks (#1273) e vive num lugar
|
||||
# só, `scripts/migration-populacao.sh`. Sem a biblioteca (clone antigo, ou a
|
||||
# cópia da skill de uma versão anterior), o pré-voo segue medindo o que media —
|
||||
# e a linha de migration DIZ que não achou a população nova.
|
||||
BIBLIOTECA="$raiz/scripts/migration-populacao.sh"
|
||||
[ -r "$BIBLIOTECA" ] && { . "$BIBLIOTECA" || true; }
|
||||
|
||||
ok() { printf '✓ %s\n' "$*"; }
|
||||
olhe() { printf '⚠ %s\n' "$*"; }
|
||||
trava(){ printf '✗ %s\n' "$*"; }
|
||||
@@ -86,16 +93,75 @@ else
|
||||
tem_mani="$(echo "$diff_nomes" | grep -cx 'supabase/migrations/MANIFEST.md' || true)"
|
||||
[ "$tem_base" = 1 ] && ok "migration nova COM apêndice no baseline.sql" || trava "migration nova SEM apêndice em supabase/baseline.sql — o kit self-host aplica só o baseline; sem ele a mudança não chega em quem instalou"
|
||||
[ "$tem_mani" = 1 ] && ok "migration nova COM linha no MANIFEST.md" || trava "migration nova SEM linha em supabase/migrations/MANIFEST.md"
|
||||
existentes="$(git ls-tree -r --name-only origin/main -- supabase/migrations | sed 's#^supabase/migrations/##')"
|
||||
# A POPULAÇÃO da unicidade: a main do PRODUTO (o remoto que aponta para
|
||||
# melgarafael/DeskcommCRM, com qualquer nome) mais `refs/heads` E
|
||||
# `refs/remotes` (#1273). O `origin/main` de antes, num clone de fork, é a main
|
||||
# DO FORK — que pode estar atrás da do principal, e aí o pré-voo dizia "NNNN
|
||||
# livre" com exit 0 sobre um número que o principal já tinha (caso medido:
|
||||
# fork parado em 0323, principal em 0324).
|
||||
pop_base=""
|
||||
if declare -F pop_main_do_produto >/dev/null 2>&1; then
|
||||
pop_base="$(pop_main_do_produto || true)"
|
||||
if [ -n "$pop_base" ] && ! git rev-parse -q --verify "${pop_base}^{commit}" >/dev/null 2>&1; then
|
||||
pop_base=""
|
||||
fi
|
||||
fi
|
||||
# pop_sem_produto=1: há remoto de GitHub (um fork) e nenhum é o principal. Aí
|
||||
# nenhum "livre" sai com ✓: o fork pode estar atrás do principal, e o ✓ sobre
|
||||
# ele é o "livre" falso da #1273. (Fixture sem remoto de GitHub só tem o que
|
||||
# tem; biblioteca ausente já sai declarada acima.)
|
||||
pop_sem_produto=0
|
||||
if [ -z "$pop_base" ]; then
|
||||
if declare -F pop_tem_remoto_de_github >/dev/null 2>&1 && pop_tem_remoto_de_github; then
|
||||
pop_sem_produto=1
|
||||
fi
|
||||
pop_base="origin/main"
|
||||
git rev-parse -q --verify "${pop_base}^{commit}" >/dev/null 2>&1 || pop_base=""
|
||||
fi
|
||||
pop_refs=""
|
||||
if declare -F pop_refs_de_outrem >/dev/null 2>&1; then
|
||||
pop_refs="$(pop_refs_de_outrem "$pop_base" 2>/dev/null || true)"
|
||||
else
|
||||
pop_refs="origin/main $(git for-each-ref --format='%(refname)' refs/heads refs/remotes 2>/dev/null || true)"
|
||||
olhe "NÃO MEDIDO no formato novo: scripts/migration-populacao.sh AUSENTE — a unicidade abaixo sai sobre origin/main + refs/heads + refs/remotes (#1273)"
|
||||
fi
|
||||
[ -n "${pop_refs// /}" ] || pop_refs="HEAD"
|
||||
pop_migs=""
|
||||
if declare -F pop_migrations >/dev/null 2>&1; then
|
||||
pop_migs="$(pop_migrations $pop_refs 2>/dev/null || true)"
|
||||
else
|
||||
# Sem a biblioteca, a lista é a de sempre: "<ref> <nome>", montada na mão,
|
||||
# para o `grep` de baixo achar a mesma coisa que achava antes.
|
||||
for ref in $pop_refs HEAD; do
|
||||
[ -n "$ref" ] || continue
|
||||
arquivos="$(git ls-tree -r --name-only "$ref" -- supabase/migrations 2>/dev/null \
|
||||
| sed 's#^supabase/migrations/##' || true)"
|
||||
[ -n "$arquivos" ] && pop_migs="${pop_migs}$(awk -v r="$ref" '{ print r, $0 }' <<<"$arquivos")"$'\n'
|
||||
done
|
||||
fi
|
||||
if [ "$pop_sem_produto" = 1 ]; then
|
||||
olhe "NÃO MEDIDO contra a main do PRODUTO: nenhum remoto aponta para melgarafael/DeskcommCRM — a unicidade abaixo sai sobre '${pop_base:-nenhuma base}' e a cópia local. Corrija com: git remote add upstream https://github.com/melgarafael/DeskcommCRM.git && git fetch upstream"
|
||||
fi
|
||||
# "livre" sobre a base errada não é ✓: é ⚠ com o nome da régua.
|
||||
livre() { if [ "$pop_sem_produto" = 1 ]; then olhe "$* (NÃO MEDIDO contra a main do produto)"; else ok "$*"; fi; }
|
||||
for m in $migs; do
|
||||
nome="$(basename "$m")"
|
||||
nnnn="$(sed -nE 's/^[0-9]{14}_([0-9]{4})_.+\.sql$/\1/p' <<<"$nome")"
|
||||
ts="$(sed -nE 's/^([0-9]{14})_[0-9]{4}_.+\.sql$/\1/p' <<<"$nome")"
|
||||
if [ -z "$nnnn" ]; then trava "'$nome' não segue <timestamp14>_<NNNN>_<slug>.sql"; continue; fi
|
||||
cn="$(grep -E "^[0-9]{14}_${nnnn}_" <<<"$existentes" | grep -vx "$nome" || true)"
|
||||
ct="$(grep -E "^${ts}_" <<<"$existentes" | grep -vx "$nome" || true)"
|
||||
[ -z "$cn" ] && ok "NNNN $nnnn livre na origin/main" || trava "NNNN $nnnn já existe na origin/main ($cn) — renumere E troque o timestamp (date -u +%Y%m%d%H%M%S)"
|
||||
[ -z "$ct" ] && ok "timestamp $ts livre na origin/main" || trava "timestamp $ts já existe na origin/main ($ct)"
|
||||
# A âncora é a POSIÇÃO do nome canônico (`^[0-9]{14}_NNNN_`), e o `grep` roda
|
||||
# sobre a LINHA INTEIRA ("<ref> <nome>") para o dono poder ser nomeado. O
|
||||
# `^[0-9]{14}_${nnnn}_` sem o campo da ref casaria a linha errada em silêncio.
|
||||
cn="$(grep -E "^[A-Za-z0-9_./-]+ [0-9]{14}_${nnnn}_.+\.sql$" <<<"$pop_migs" | grep -vE " ${nome}\$" || true)"
|
||||
ct="$(grep -E "^[A-Za-z0-9_./-]+ ${ts}_[0-9]{4}_.+\.sql$" <<<"$pop_migs" | grep -vE " ${nome}\$" || true)"
|
||||
dono="$(awk '{printf "%s(%s) ", $1, $2}' <<<"$cn" | sed 's/ $//')"
|
||||
# "livre" sem dizer sobre QUAL população é a afirmação sem régua que a
|
||||
# #1155 registrou — e o número que se renumera errado sai daqui.
|
||||
pop_desc="$pop_base ∪ outras refs do clone"
|
||||
[ -z "$cn" ] && livre "NNNN $nnnn livre em $pop_desc" \
|
||||
|| trava "NNNN $nnnn já existe em $pop_desc: $dono — renumere E troque o timestamp (date -u +%Y%m%d%H%M%S)"
|
||||
[ -z "$ct" ] && livre "timestamp $ts livre em $pop_desc" \
|
||||
|| trava "timestamp $ts já existe em $pop_desc: $(awk '{printf "%s(%s) ", $1, $2}' <<<"$ct" | sed 's/ $//')"
|
||||
if grep -qiE 'create table' "$m" && ! grep -qiE 'enable row level security' "$m"; then
|
||||
olhe "'$nome' cria tabela sem 'enable row level security' — tabela tenant-aware exige RLS + policy tenant_isolation_<tabela>_all + entrada em tests/invariants/rls-isolation.test.ts"
|
||||
fi
|
||||
@@ -103,7 +169,31 @@ else
|
||||
olhe "'$nome' cria função sem 'revoke execute … from public, anon' — função nova em public nasce exposta como RPC"
|
||||
fi
|
||||
done
|
||||
olhe "colisão com PR ABERTO não é visível daqui: se acontecer, quem renumera é o mantenedor (é o combinado do template de PR)"
|
||||
# O resto da população — os PRs ABERTOS, inclusive de fork — NÃO é medido aqui:
|
||||
# o pré-voo não vai à rede. Declarar é o contrato; com o `gh` logado, o mesmo
|
||||
# `gh pr list` do #1269 fecha a conta (issue #1273).
|
||||
pop_teto="$(cut -d' ' -f2- <<<"$pop_migs" | sed -nE 's/^[0-9]{14}_([0-9]{4})_.*$/\1/p' | sort -n | tail -1)"
|
||||
if [ -n "$pop_teto" ]; then
|
||||
pop_prox="$(printf '%04d' $((10#$pop_teto + 1)))"
|
||||
livre "próximo NNNN medido em $pop_desc: ${pop_prox} (teto ${pop_teto})"
|
||||
else
|
||||
nao "nenhum NNNN entrou na população medida (base '${pop_base:-nenhuma}'; PRs abertos fora daqui — use pnpm checar:colisao-de-migration)"
|
||||
fi
|
||||
# A fila de PRs abertos é do repositório do PRODUTO, com qualquer nome de
|
||||
# remoto — é o mesmo cuidado do #1269: listar o fork devolve zero, e esse zero
|
||||
# sairia como medição.
|
||||
pop_repo="$(pop_repo_do_produto 2>/dev/null || true)"
|
||||
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
||||
if [ -n "$pop_repo" ]; then
|
||||
pop_abertos="$(gh pr list --repo "$pop_repo" --state open --limit 1000 \
|
||||
--json number --jq '.[].number' 2>/dev/null | grep -c . || true)"
|
||||
ok "PRs abertos em $pop_repo: ${pop_abertos:-0} visível(is) — a prévia do número é 'pnpm checar:colisao-de-migration' (mede também as cabeças, inclusive de fork)"
|
||||
else
|
||||
olhe "PRs ABERTOS NÃO MEDIDOS: este clone não tem remoto do repositório principal. Quem mede: pnpm checar:colisao-de-migration"
|
||||
fi
|
||||
else
|
||||
olhe "PRs ABERTOS (inclusive de fork) NÃO MEDIDOS: o gh não está logado aqui. Quem mede é o script: pnpm checar:colisao-de-migration"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 4. Release: fragmento sim, CHANGELOG à mão não ──────────────────────────
|
||||
|
||||
@@ -44,10 +44,15 @@ código) + `docs/specs/` — este agente só existe pra executar dentro dela.
|
||||
`supabase/migrations/<timestamp>_<NNNN>_<slug>.sql` + apêndice idempotente em
|
||||
`supabase/baseline.sql` (bloco `-- ---- <coisa> (migration NNNN) ----`) + linha
|
||||
em `supabase/migrations/MANIFEST.md` + `lib/database.types.ts` regenerado.
|
||||
O próximo `NNNN` é verificado contra TODAS as branches locais
|
||||
(`git branch --format='%(refname:short)'` + `git ls-tree` — a cadeia
|
||||
`vendaval/F2-*` tem migrations não mergeadas). Um hook de pre-commit barra a
|
||||
tripla incompleta — não tente contorná-lo.
|
||||
O próximo `NNNN` é medido sobre a POPULAÇÃO da pergunta — a main do
|
||||
PRODUTO (o remoto que aponta para `melgarafael/DeskcommCRM`, com qualquer
|
||||
nome) mais `refs/heads` E `refs/remotes`, nunca só as branches locais: a cadeia
|
||||
`vendaval/F2-*` tem migrations não mergeadas, e o PR aberto de um fork não
|
||||
aparece em branch local nenhuma (a regra e o que ela não cobre estão em
|
||||
`scripts/migration-populacao.sh`, e o que mede também os PRs abertos é
|
||||
`pnpm checar:colisao-de-migration`). O `NNNN` sai da posição do nome
|
||||
canônico (`^[0-9]{14}_([0-9]{4})_`), nunca de um `_NNNN_` do slug. Um hook de
|
||||
pre-commit barra a tripla incompleta — não tente contorná-lo.
|
||||
- **Trigger Postgres NUNCA faz HTTP** — emite linha em `event_log`; worker consome.
|
||||
- **Idempotência**: `unique (organization_id, external_id)` + captura `23505`.
|
||||
- **Audit em mutação relevante**: POST/PATCH/DELETE bem-sucedido → `api_audit_log`
|
||||
|
||||
@@ -43,8 +43,12 @@ dessa vez". Aprovar por cortesia é a única falha inaceitável no seu papel.
|
||||
atendente.
|
||||
- **(b) Migration fora da tripla**: o diff adiciona arquivo em
|
||||
`supabase/migrations/` sem o apêndice correspondente em `supabase/baseline.sql`
|
||||
E a linha no `supabase/migrations/MANIFEST.md`? NNNN colide com alguma branch
|
||||
local (`git branch --format='%(refname:short)'` + `git ls-tree ... supabase/migrations`)?
|
||||
E a linha no `supabase/migrations/MANIFEST.md`? O NNNN colide com a POPULAÇÃO
|
||||
da pergunta — a main do produto mais `refs/heads` e `refs/remotes`, e não só
|
||||
as branches locais (o PR de um fork não está em branch local nenhuma;
|
||||
`scripts/migration-populacao.sh`) — ou com um PR ABERTO, inclusive de fork
|
||||
(`pnpm checar:colisao-de-migration`)? O NNNN saiu da posição do nome canônico
|
||||
(`^[0-9]{14}_([0-9]{4})_`) e não de um `_NNNN_` do slug?
|
||||
`lib/database.types.ts` ficou defasado? Qualquer um = FAIL.
|
||||
- Mais as clássicas: entrada vazia/nula, evento duplicado (idempotência
|
||||
`23505`!), restart no meio (estado sobrevive?), concorrência (2 claims da
|
||||
|
||||
@@ -92,8 +92,13 @@ bash .agents/skills/deskcomm-contribuir/scripts/armar-hooks.sh
|
||||
```
|
||||
|
||||
Três guardas, e só isso: `pre-commit` reprova migration nova sem apêndice no `baseline.sql` e sem
|
||||
linha no `MANIFEST.md` no mesmo commit, e número (`NNNN`) ou timestamp já usado na `origin/main`
|
||||
ou em branch local; `pre-push` reprova push na `main`; e os dois avisam (sem bloquear) quando o
|
||||
linha no `MANIFEST.md` no mesmo commit, e número (`NNNN`) ou timestamp já usado na **população da
|
||||
pergunta** — a main do PRODUTO (o remoto que aponta para `melgarafael/DeskcommCRM`, com qualquer
|
||||
nome; num fork, a sua `origin/main` é a main do fork e não vale) mais `refs/heads` e `refs/remotes`.
|
||||
O que essa população não cobre são os PRs abertos, e a mensagem do hook diz isso e aponta
|
||||
`pnpm checar:colisao-de-migration`, que mede também eles. O `NNNN` sai da posição do nome canônico
|
||||
(`^[0-9]{14}_([0-9]{4})_`), e a regra está em `migration-populacao.sh`, na pasta `scripts/` da raiz do repositório (issue #1273);
|
||||
`pre-push` reprova push na `main`; e os dois avisam (sem bloquear) quando o
|
||||
commit está assinado como `root@…` ou sem e-mail — trabalho assinado assim não aparece no perfil
|
||||
do GitHub de quem fez. O mantenedor roda hooks próprios (`loop/hooks`); o script recusa
|
||||
sobrescrevê-los.
|
||||
|
||||
@@ -54,7 +54,11 @@ A tripla é indivisível: arquivo em `supabase/migrations/`, apêndice **idempot
|
||||
`create or replace function`, `drop policy if exists` + `create policy`), linha no `MANIFEST.md`.
|
||||
O kit self-host aplica **só o baseline** — migration sem apêndice não existe para quem instalou.
|
||||
|
||||
- `NNNN` e timestamp únicos contra `origin/main` (o hook confere; o script também). Ao renumerar,
|
||||
- `NNNN` e timestamp únicos contra a POPULAÇÃO da pergunta: a main do PRODUTO (o remoto que
|
||||
aponta para `melgarafael/DeskcommCRM`, com qualquer nome — num fork, `origin/main` é a main do
|
||||
fork) mais `refs/heads` e `refs/remotes`; os PRs abertos saem por fora, declarados, e quem os
|
||||
mede é `pnpm checar:colisao-de-migration` (issue #1273). O `NNNN` sai da posição do nome
|
||||
canônico (`^[0-9]{14}_([0-9]{4})_`), nunca de um `_NNNN_` do slug. Ao renumerar,
|
||||
troque o timestamp junto: renumerar só o `NNNN` é o que fabrica colisão de timestamp.
|
||||
- Tabela tenant-aware: `organization_id uuid not null references organizations(id) on delete cascade`,
|
||||
`enable row level security`, policy `tenant_isolation_<tabela>_all` via `fn_user_org_ids()`, e a
|
||||
|
||||
@@ -5,21 +5,98 @@
|
||||
# 1. mudança em supabase/baseline.sql (apêndice idempotente — é o que o kit
|
||||
# self-host aplica; migration que não chega lá não chega em quem instalou)
|
||||
# 2. linha em supabase/migrations/MANIFEST.md
|
||||
# E o NNNN e o TIMESTAMP do nome novo não podem existir em origin/main nem em
|
||||
# branch local: colisão é o defeito nº 1 da triagem (renumerada 11 vezes desde
|
||||
# agosto de 2026), e quem cria migration copiando outra copia os dois.
|
||||
# E o NNNN e o TIMESTAMP do nome novo não podem existir na POPULAÇÃO que os
|
||||
# mediu: a main do PRODUTO (o remoto que aponta para melgarafael/DeskcommCRM, com
|
||||
# qualquer nome) mais as outras refs do clone. Colisão é o defeito nº 1 da
|
||||
# triagem (renumerada 11 vezes desde agosto de 2026), e quem cria migration
|
||||
# copiando outra copia os dois. A população — e o que ela DEIXA de fora — é a
|
||||
# mesma dos outros sete lugares (#1273), em `scripts/migration-populacao.sh`.
|
||||
#
|
||||
# Diferença para o hook do mantenedor (loop/hooks): compara contra origin/main
|
||||
# (um fork raramente tem outras branches locais) e não conhece a dívida de
|
||||
# timestamp da main, que é assunto do mantenedor.
|
||||
# Diferença para o hook do mantenedor (loop/hooks): este não conhece a dívida
|
||||
# de timestamp da main, que é assunto do mantenedor.
|
||||
# Bypass explícito (correção orientada pelo mantenedor): DESKCOMM_MIGRATION_EDIT=1
|
||||
set -euo pipefail
|
||||
|
||||
[ "${DESKCOMM_MIGRATION_EDIT:-0}" = "1" ] && exit 0
|
||||
|
||||
top="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
|
||||
novas="$(git diff --cached --name-status | awk '$1 == "A" && $2 ~ /^supabase\/migrations\/.*\.sql$/ { print $2 }')"
|
||||
# ── A POPULAÇÃO da unicidade (issue #1273) ──────────────────────────────────────
|
||||
#
|
||||
# A pergunta deste hook é "esse número já foi usado ou reservado?", e a resposta
|
||||
# honesta é sobre a main do PRODUTO (o remoto que aponta para
|
||||
# melgarafael/DeskcommCRM, com QUALQUER nome) mais as outras refs do clone. A
|
||||
# versão anterior media `origin/main` + `git branch` — e essas duas são o
|
||||
# recorte que a issue mediu como errado: num clone de fork, a `origin/main` é a
|
||||
# main DO FORK (que pode estar atrás da do principal, e o hook saía com 0
|
||||
# dizendo que o número estava livre), e `git branch` só via branches LOCAIS.
|
||||
# A cabeça de um PR aberto de fork vive em `refs/pull/N/head` no repositório
|
||||
# pai, e no clone do mantenedor o 0269 do #965 aparecia só como cópia em
|
||||
# `refs/remotes/origin/pr/965` — nenhuma das 188 branches locais tinha esse
|
||||
# número. A cópia em `refs/remotes/*/pr/N` fica de fora de propósito: é cópia
|
||||
# de cabeça, não PR aberto, e sobrevive ao fechamento (965 cópias contra 43 PRs
|
||||
# abertos, medido em 19/09/2026) — os PRs abertos entram pela LISTA do `gh`, no
|
||||
# `pnpm checar:colisao-de-migration`.
|
||||
#
|
||||
# `pop_tem_nnnn`/`pop_ts_em_uso` casam o NNNN e o timestamp na POSIÇÃO do nome
|
||||
# canônico, e não em qualquer "_NNNN_": é a âncora do #1269, e a variante
|
||||
# gulosa pegava o número do SLUG (`…_0326_relatorio_2024_anual.sql` virava
|
||||
# 2024) e perdia a duplicata real de 0326.
|
||||
#
|
||||
# A POPULAÇÃO é a mesma dos outros sete lugares, com a regra de degradação
|
||||
# declarada: a base é a main do PRODUTO quando algum remoto é o principal; sem
|
||||
# ele, é a `origin/main` que houver — e a saída DIZ qual das duas foi. As
|
||||
# branches LOCAIS e REMOTAS entram sempre (a de antes era só a local). O 0269 do
|
||||
# #965 continua FORA desta conta: ele morava só na cópia `pr/965`, que fica
|
||||
# excluída de propósito; quem o pega é o `pnpm checar:colisao-de-migration`.
|
||||
BIBLIOTECA="$top/scripts/migration-populacao.sh"
|
||||
if [ -r "$BIBLIOTECA" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$BIBLIOTECA" || true
|
||||
fi
|
||||
aviso_fork=0
|
||||
base=""
|
||||
if declare -F pop_main_do_produto >/dev/null 2>&1; then
|
||||
base="$(pop_main_do_produto || true)"
|
||||
[ -n "$base" ] || base=""
|
||||
if [ -n "$base" ] && ! git rev-parse -q --verify "${base}^{commit}" >/dev/null 2>&1; then
|
||||
base=""
|
||||
fi
|
||||
fi
|
||||
if [ -z "$base" ]; then
|
||||
# Sem o principal no clone (fork sem `upstream`, ou biblioteca ausente): a
|
||||
# base cai para a `origin/main`, e o hook sai DIZENDO que a população não é a
|
||||
# da pergunta completa. Encolher o universo em silêncio é o defeito da #1273.
|
||||
# Só avisa quando HÁ remoto de GitHub (um fork): fixture sem remoto nenhum só
|
||||
# tem o que tem. Sem a biblioteca, o aviso de ausência mais abaixo já diz.
|
||||
if declare -F pop_tem_remoto_de_github >/dev/null 2>&1 && pop_tem_remoto_de_github; then
|
||||
aviso_fork=1
|
||||
fi
|
||||
base="origin/main"
|
||||
git rev-parse -q --verify "${base}^{commit}" >/dev/null 2>&1 || base=""
|
||||
fi
|
||||
|
||||
# O que já está na main do PRODUTO não é novidade deste commit. Um `git merge
|
||||
# upstream/main` encena as migrations da main como `A` relativo à branch, e a
|
||||
# população alargada (refs/remotes/*) tem branches do principal com OUTRO
|
||||
# arquivo de mesmo NNNN (17 NNNN da main em 31 branches do upstream, medido em
|
||||
# 27/09/2026): sem este filtro o merge da main era BLOQUEADO mandando renumerar
|
||||
# arquivo que todo mundo já tem — a orientação impossível que o loop/hooks
|
||||
# registrou (18/09/2026). É o mesmo filtro dele, sobre a base do PRODUTO.
|
||||
if [ -n "$base" ]; then
|
||||
novas="$(while IFS= read -r p; do
|
||||
[ -z "$p" ] && continue
|
||||
git cat-file -e "$base:$p" 2>/dev/null || printf '%s\n' "$p"
|
||||
done <<<"$novas")"
|
||||
fi
|
||||
|
||||
[ -z "$novas" ] && exit 0
|
||||
|
||||
if [ "$aviso_fork" = 1 ]; then
|
||||
echo "pre-commit AVISO: NNNN/timestamp NÃO MEDIDOS contra a main do PRODUTO: nenhum remoto aponta para melgarafael/DeskcommCRM (a base é a origin/main do seu fork, que pode estar atrás). Corrija com: git remote add upstream https://github.com/melgarafael/DeskcommCRM.git && git fetch upstream (#1273)" >&2
|
||||
fi
|
||||
|
||||
staged="$(git diff --cached --name-only)"
|
||||
falhou=0
|
||||
|
||||
@@ -33,10 +110,50 @@ if ! grep -qx 'supabase/migrations/MANIFEST.md' <<<"$staged"; then
|
||||
falhou=1
|
||||
fi
|
||||
|
||||
# Refs contra as quais a unicidade é medida: origin/main (se existe) + branches locais.
|
||||
refs="$(git branch --format='%(refname:short)' 2>/dev/null || true)"
|
||||
if git rev-parse -q --verify origin/main >/dev/null 2>&1; then refs="origin/main
|
||||
$refs"; fi
|
||||
if declare -F pop_refs_de_outrem >/dev/null 2>&1; then
|
||||
refs="$(pop_refs_de_outrem "$base" 2>/dev/null || true)"
|
||||
else
|
||||
refs="origin/main $(git for-each-ref --format='%(refname)' refs/heads refs/remotes 2>/dev/null || true)"
|
||||
fi
|
||||
# A lista que entrou é a POPULAÇÃO que o grep vai casar. Ela NUNCA pode ficar
|
||||
# vazia por acidente: um `pop_migrations` que sai vazio (biblioteca ausente,
|
||||
# clone sem nenhuma migration, `git` fora do repositório) transformaria o
|
||||
# `grep` de baixo em "não há colisão" e o hook LIBERARIA o commit — um guard
|
||||
# que encolhe o universo em silêncio é exatamente o defeito que a #1273 corrige.
|
||||
# A degradação declarada é a de antes (medir a `origin/main` e as refs que houver)
|
||||
# com um aviso, nunca a população zerada.
|
||||
# `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=""
|
||||
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)"
|
||||
fi
|
||||
if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; 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
|
||||
fallback=""
|
||||
for ref in $refs HEAD; do
|
||||
[ -n "$ref" ] || continue
|
||||
arquivos="$(git ls-tree -r --name-only "$ref" -- supabase/migrations 2>/dev/null \
|
||||
| sed 's#^supabase/migrations/##' || true)"
|
||||
# Prefixa CADA linha com a ref: o grep de baixo casa "<ref> <nome>", e
|
||||
# prefixar só a 1ª deixava passar colisão com qualquer arquivo que não
|
||||
# fosse o primeiro da ref.
|
||||
[ -n "$arquivos" ] && fallback="${fallback}$(awk -v r="$ref" '{ print r, $0 }' <<<"$arquivos")"$'\n'
|
||||
done
|
||||
populacao="$fallback"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
while IFS= read -r caminho; do
|
||||
nome="$(basename "$caminho")"
|
||||
@@ -47,23 +164,31 @@ while IFS= read -r caminho; do
|
||||
falhou=1
|
||||
continue
|
||||
fi
|
||||
while IFS= read -r ref; do
|
||||
[ -z "$ref" ] && continue
|
||||
existentes="$(git ls-tree -r --name-only "$ref" -- supabase/migrations 2>/dev/null | sed 's#^supabase/migrations/##' || true)"
|
||||
colisao_n="$(grep -E "^[0-9]{14}_${nnnn}_.+\.sql$" <<<"$existentes" | grep -vx "$nome" || true)"
|
||||
colisao_t="$(grep -E "^${ts}_[0-9]{4}_.+\.sql$" <<<"$existentes" | grep -vx "$nome" || true)"
|
||||
if [ -n "$colisao_n" ]; then
|
||||
echo "pre-commit BLOQUEADO: NNNN=$nnnn de '$nome' já existe em '$ref': $colisao_n" >&2
|
||||
echo " Próximo livre: git ls-tree -r --name-only origin/main -- supabase/migrations | sed -E 's/.*_([0-9]{4})_.*/\\1/' | sort -n | tail -1" >&2
|
||||
echo " Troque o TIMESTAMP junto (date -u +%Y%m%d%H%M%S) — renumerar só o NNNN é o que fabrica colisão de timestamp." >&2
|
||||
falhou=1
|
||||
# O MESMO arquivo nesta população é o PR de quem roda (o índice ainda o
|
||||
# mostra como `A`): ele não é colisão, e acusar o autor de colidir com a
|
||||
# própria branch é a armadilha que a #1155 registrou.
|
||||
# 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)"
|
||||
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
|
||||
else
|
||||
echo " Para o próximo número livre (main do produto ∪ PRs abertos): pnpm checar:colisao-de-migration" >&2
|
||||
fi
|
||||
if [ -n "$colisao_t" ]; then
|
||||
echo "pre-commit BLOQUEADO: timestamp $ts de '$nome' já existe em '$ref': $colisao_t" >&2
|
||||
echo " O Supabase usa o timestamp como identidade da migration; dois iguais quebram db push/reset." >&2
|
||||
falhou=1
|
||||
fi
|
||||
done <<<"$refs"
|
||||
echo " Troque o TIMESTAMP junto (date -u +%Y%m%d%H%M%S) — renumerar só o NNNN é o que fabrica colisão de timestamp." >&2
|
||||
falhou=1
|
||||
fi
|
||||
if [ -n "$donos_t" ]; then
|
||||
onde="$(awk '{printf "%s(%s) ", $1, $2}' <<<"$donos_t" | sed 's/ $//')"
|
||||
echo "pre-commit BLOQUEADO: timestamp $ts de '$nome' já existe em: $onde" >&2
|
||||
echo " O Supabase usa o timestamp como identidade da migration; dois iguais quebram db push/reset." >&2
|
||||
falhou=1
|
||||
fi
|
||||
done <<<"$novas"
|
||||
|
||||
if [ "$falhou" = 1 ]; then
|
||||
|
||||
@@ -14,6 +14,13 @@ set -uo pipefail
|
||||
raiz="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "✗ fora de um clone git"; exit 0; }
|
||||
cd "$raiz"
|
||||
|
||||
# A POPULAÇÃO da unicidade de NNNN é a mesma dos hooks (#1273) e vive num lugar
|
||||
# só, `scripts/migration-populacao.sh`. Sem a biblioteca (clone antigo, ou a
|
||||
# cópia da skill de uma versão anterior), o pré-voo segue medindo o que media —
|
||||
# e a linha de migration DIZ que não achou a população nova.
|
||||
BIBLIOTECA="$raiz/scripts/migration-populacao.sh"
|
||||
[ -r "$BIBLIOTECA" ] && { . "$BIBLIOTECA" || true; }
|
||||
|
||||
ok() { printf '✓ %s\n' "$*"; }
|
||||
olhe() { printf '⚠ %s\n' "$*"; }
|
||||
trava(){ printf '✗ %s\n' "$*"; }
|
||||
@@ -86,16 +93,75 @@ else
|
||||
tem_mani="$(echo "$diff_nomes" | grep -cx 'supabase/migrations/MANIFEST.md' || true)"
|
||||
[ "$tem_base" = 1 ] && ok "migration nova COM apêndice no baseline.sql" || trava "migration nova SEM apêndice em supabase/baseline.sql — o kit self-host aplica só o baseline; sem ele a mudança não chega em quem instalou"
|
||||
[ "$tem_mani" = 1 ] && ok "migration nova COM linha no MANIFEST.md" || trava "migration nova SEM linha em supabase/migrations/MANIFEST.md"
|
||||
existentes="$(git ls-tree -r --name-only origin/main -- supabase/migrations | sed 's#^supabase/migrations/##')"
|
||||
# A POPULAÇÃO da unicidade: a main do PRODUTO (o remoto que aponta para
|
||||
# melgarafael/DeskcommCRM, com qualquer nome) mais `refs/heads` E
|
||||
# `refs/remotes` (#1273). O `origin/main` de antes, num clone de fork, é a main
|
||||
# DO FORK — que pode estar atrás da do principal, e aí o pré-voo dizia "NNNN
|
||||
# livre" com exit 0 sobre um número que o principal já tinha (caso medido:
|
||||
# fork parado em 0323, principal em 0324).
|
||||
pop_base=""
|
||||
if declare -F pop_main_do_produto >/dev/null 2>&1; then
|
||||
pop_base="$(pop_main_do_produto || true)"
|
||||
if [ -n "$pop_base" ] && ! git rev-parse -q --verify "${pop_base}^{commit}" >/dev/null 2>&1; then
|
||||
pop_base=""
|
||||
fi
|
||||
fi
|
||||
# pop_sem_produto=1: há remoto de GitHub (um fork) e nenhum é o principal. Aí
|
||||
# nenhum "livre" sai com ✓: o fork pode estar atrás do principal, e o ✓ sobre
|
||||
# ele é o "livre" falso da #1273. (Fixture sem remoto de GitHub só tem o que
|
||||
# tem; biblioteca ausente já sai declarada acima.)
|
||||
pop_sem_produto=0
|
||||
if [ -z "$pop_base" ]; then
|
||||
if declare -F pop_tem_remoto_de_github >/dev/null 2>&1 && pop_tem_remoto_de_github; then
|
||||
pop_sem_produto=1
|
||||
fi
|
||||
pop_base="origin/main"
|
||||
git rev-parse -q --verify "${pop_base}^{commit}" >/dev/null 2>&1 || pop_base=""
|
||||
fi
|
||||
pop_refs=""
|
||||
if declare -F pop_refs_de_outrem >/dev/null 2>&1; then
|
||||
pop_refs="$(pop_refs_de_outrem "$pop_base" 2>/dev/null || true)"
|
||||
else
|
||||
pop_refs="origin/main $(git for-each-ref --format='%(refname)' refs/heads refs/remotes 2>/dev/null || true)"
|
||||
olhe "NÃO MEDIDO no formato novo: scripts/migration-populacao.sh AUSENTE — a unicidade abaixo sai sobre origin/main + refs/heads + refs/remotes (#1273)"
|
||||
fi
|
||||
[ -n "${pop_refs// /}" ] || pop_refs="HEAD"
|
||||
pop_migs=""
|
||||
if declare -F pop_migrations >/dev/null 2>&1; then
|
||||
pop_migs="$(pop_migrations $pop_refs 2>/dev/null || true)"
|
||||
else
|
||||
# Sem a biblioteca, a lista é a de sempre: "<ref> <nome>", montada na mão,
|
||||
# para o `grep` de baixo achar a mesma coisa que achava antes.
|
||||
for ref in $pop_refs HEAD; do
|
||||
[ -n "$ref" ] || continue
|
||||
arquivos="$(git ls-tree -r --name-only "$ref" -- supabase/migrations 2>/dev/null \
|
||||
| sed 's#^supabase/migrations/##' || true)"
|
||||
[ -n "$arquivos" ] && pop_migs="${pop_migs}$(awk -v r="$ref" '{ print r, $0 }' <<<"$arquivos")"$'\n'
|
||||
done
|
||||
fi
|
||||
if [ "$pop_sem_produto" = 1 ]; then
|
||||
olhe "NÃO MEDIDO contra a main do PRODUTO: nenhum remoto aponta para melgarafael/DeskcommCRM — a unicidade abaixo sai sobre '${pop_base:-nenhuma base}' e a cópia local. Corrija com: git remote add upstream https://github.com/melgarafael/DeskcommCRM.git && git fetch upstream"
|
||||
fi
|
||||
# "livre" sobre a base errada não é ✓: é ⚠ com o nome da régua.
|
||||
livre() { if [ "$pop_sem_produto" = 1 ]; then olhe "$* (NÃO MEDIDO contra a main do produto)"; else ok "$*"; fi; }
|
||||
for m in $migs; do
|
||||
nome="$(basename "$m")"
|
||||
nnnn="$(sed -nE 's/^[0-9]{14}_([0-9]{4})_.+\.sql$/\1/p' <<<"$nome")"
|
||||
ts="$(sed -nE 's/^([0-9]{14})_[0-9]{4}_.+\.sql$/\1/p' <<<"$nome")"
|
||||
if [ -z "$nnnn" ]; then trava "'$nome' não segue <timestamp14>_<NNNN>_<slug>.sql"; continue; fi
|
||||
cn="$(grep -E "^[0-9]{14}_${nnnn}_" <<<"$existentes" | grep -vx "$nome" || true)"
|
||||
ct="$(grep -E "^${ts}_" <<<"$existentes" | grep -vx "$nome" || true)"
|
||||
[ -z "$cn" ] && ok "NNNN $nnnn livre na origin/main" || trava "NNNN $nnnn já existe na origin/main ($cn) — renumere E troque o timestamp (date -u +%Y%m%d%H%M%S)"
|
||||
[ -z "$ct" ] && ok "timestamp $ts livre na origin/main" || trava "timestamp $ts já existe na origin/main ($ct)"
|
||||
# A âncora é a POSIÇÃO do nome canônico (`^[0-9]{14}_NNNN_`), e o `grep` roda
|
||||
# sobre a LINHA INTEIRA ("<ref> <nome>") para o dono poder ser nomeado. O
|
||||
# `^[0-9]{14}_${nnnn}_` sem o campo da ref casaria a linha errada em silêncio.
|
||||
cn="$(grep -E "^[A-Za-z0-9_./-]+ [0-9]{14}_${nnnn}_.+\.sql$" <<<"$pop_migs" | grep -vE " ${nome}\$" || true)"
|
||||
ct="$(grep -E "^[A-Za-z0-9_./-]+ ${ts}_[0-9]{4}_.+\.sql$" <<<"$pop_migs" | grep -vE " ${nome}\$" || true)"
|
||||
dono="$(awk '{printf "%s(%s) ", $1, $2}' <<<"$cn" | sed 's/ $//')"
|
||||
# "livre" sem dizer sobre QUAL população é a afirmação sem régua que a
|
||||
# #1155 registrou — e o número que se renumera errado sai daqui.
|
||||
pop_desc="$pop_base ∪ outras refs do clone"
|
||||
[ -z "$cn" ] && livre "NNNN $nnnn livre em $pop_desc" \
|
||||
|| trava "NNNN $nnnn já existe em $pop_desc: $dono — renumere E troque o timestamp (date -u +%Y%m%d%H%M%S)"
|
||||
[ -z "$ct" ] && livre "timestamp $ts livre em $pop_desc" \
|
||||
|| trava "timestamp $ts já existe em $pop_desc: $(awk '{printf "%s(%s) ", $1, $2}' <<<"$ct" | sed 's/ $//')"
|
||||
if grep -qiE 'create table' "$m" && ! grep -qiE 'enable row level security' "$m"; then
|
||||
olhe "'$nome' cria tabela sem 'enable row level security' — tabela tenant-aware exige RLS + policy tenant_isolation_<tabela>_all + entrada em tests/invariants/rls-isolation.test.ts"
|
||||
fi
|
||||
@@ -103,7 +169,31 @@ else
|
||||
olhe "'$nome' cria função sem 'revoke execute … from public, anon' — função nova em public nasce exposta como RPC"
|
||||
fi
|
||||
done
|
||||
olhe "colisão com PR ABERTO não é visível daqui: se acontecer, quem renumera é o mantenedor (é o combinado do template de PR)"
|
||||
# O resto da população — os PRs ABERTOS, inclusive de fork — NÃO é medido aqui:
|
||||
# o pré-voo não vai à rede. Declarar é o contrato; com o `gh` logado, o mesmo
|
||||
# `gh pr list` do #1269 fecha a conta (issue #1273).
|
||||
pop_teto="$(cut -d' ' -f2- <<<"$pop_migs" | sed -nE 's/^[0-9]{14}_([0-9]{4})_.*$/\1/p' | sort -n | tail -1)"
|
||||
if [ -n "$pop_teto" ]; then
|
||||
pop_prox="$(printf '%04d' $((10#$pop_teto + 1)))"
|
||||
livre "próximo NNNN medido em $pop_desc: ${pop_prox} (teto ${pop_teto})"
|
||||
else
|
||||
nao "nenhum NNNN entrou na população medida (base '${pop_base:-nenhuma}'; PRs abertos fora daqui — use pnpm checar:colisao-de-migration)"
|
||||
fi
|
||||
# A fila de PRs abertos é do repositório do PRODUTO, com qualquer nome de
|
||||
# remoto — é o mesmo cuidado do #1269: listar o fork devolve zero, e esse zero
|
||||
# sairia como medição.
|
||||
pop_repo="$(pop_repo_do_produto 2>/dev/null || true)"
|
||||
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
||||
if [ -n "$pop_repo" ]; then
|
||||
pop_abertos="$(gh pr list --repo "$pop_repo" --state open --limit 1000 \
|
||||
--json number --jq '.[].number' 2>/dev/null | grep -c . || true)"
|
||||
ok "PRs abertos em $pop_repo: ${pop_abertos:-0} visível(is) — a prévia do número é 'pnpm checar:colisao-de-migration' (mede também as cabeças, inclusive de fork)"
|
||||
else
|
||||
olhe "PRs ABERTOS NÃO MEDIDOS: este clone não tem remoto do repositório principal. Quem mede: pnpm checar:colisao-de-migration"
|
||||
fi
|
||||
else
|
||||
olhe "PRs ABERTOS (inclusive de fork) NÃO MEDIDOS: o gh não está logado aqui. Quem mede é o script: pnpm checar:colisao-de-migration"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 4. Release: fragmento sim, CHANGELOG à mão não ──────────────────────────
|
||||
|
||||
@@ -559,9 +559,25 @@ Processo padrão (siga sempre):
|
||||
1. **Arquivo versionado** em `supabase/migrations/` com o padrão do repo: `<timestamp>_<NNNN>_<slug>.sql` (ex.: `20260706210000_0027_whatsapp_conversation_unification.sql`). `NNNN` é o próximo número sequencial — e **não** é o do último arquivo da listagem:
|
||||
|
||||
```bash
|
||||
ls supabase/migrations/ | grep -oE '_[0-9]{4}_' | tr -d _ | sort -n | tail -1
|
||||
# A POPULAÇÃO da pergunta: main do PRODUTO (o remoto que aponta para
|
||||
# melgarafael/DeskcommCRM, com qualquer nome) + TODO PR ABERTO, inclusive de
|
||||
# fork. O `ls` abaixo mede o DISCO, que responde uma pergunta menor.
|
||||
pnpm checar:colisao-de-migration # declara o que mediu e o que não mediu
|
||||
```
|
||||
|
||||
Se a leitura for manual, três coisas são obrigatórias: um `git fetch` antes (a árvore em dia
|
||||
não é a main atual), `git ls-tree` da main do PRODUTO e não `ls` do disco, e o `NNNN` tirado
|
||||
com a **âncora do nome canônico** aplicada ao nome **sem a pasta** —
|
||||
`sed 's#.*/##' | sed -nE 's#^[0-9]{14}_([0-9]{4})_.*#\1#p'`. O
|
||||
`ls | grep -oE '_[0-9]{4}_'` que estava aqui pegava um `_NNNN_` do SLUG (com
|
||||
`…_0326_relatorio_2024_anual.sql` o teto virava 2024) e media a árvore de
|
||||
trabalho, onde a 0336 já podia estar reservada por um PR aberto (#1273).
|
||||
|
||||
O `checar` mede o arquivo que você **já** acrescentou: sem migration nova, ele responde
|
||||
`OK — nenhuma migration acrescentada` e não dá número. Crie o arquivo com um número provisório
|
||||
e rode; ou, para alocar antes, use a enumeração do que está em voo em `triagem/TRIAGEM.md`
|
||||
(modo de falha 37). O teto é a main **mais** tudo em voo, em NNNN **e** em timestamp.
|
||||
|
||||
O nome do arquivo começa pelo **timestamp**, e timestamp e `NNNN` podem discordar: em
|
||||
09/09/2026 o `ls | tail -1` devolvia o `_0230_` (timestamp de 07/09) enquanto o maior `NNNN`
|
||||
era `_0231_` (timestamp de 05/09). Um contribuidor externo seguiu a instrução antiga ao pé da
|
||||
|
||||
@@ -10,6 +10,8 @@ set -euo pipefail
|
||||
|
||||
[ "${DESKCOMM_GOV_MIGRATION_EDIT:-0}" = "1" ] && exit 0
|
||||
|
||||
top="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
|
||||
# Migrations novas (status A) neste commit
|
||||
new_migrations=$(git diff --cached --name-status \
|
||||
| awk '$1 == "A" && $2 ~ /^supabase\/migrations\/.*\.sql$/ { print $2 }')
|
||||
@@ -65,27 +67,97 @@ fi
|
||||
# aviso. Por isso os guards abaixo ACUMULAM: reportam tudo e saem uma vez só.
|
||||
houve_conflito=0
|
||||
|
||||
# Sequência NNNN única contra TODAS as branches locais
|
||||
# ── A POPULAÇÃO da unicidade (issue #1273) ──────────────────────────────────────
|
||||
#
|
||||
# A régua que este hook ensina é "o próximo NNNN livre", e ela media `git branch`
|
||||
# — as branches LOCAIS. A população agora é a main do PRODUTO (o remoto que
|
||||
# aponta para melgarafael/DeskcommCRM, com qualquer nome) mais `refs/heads` E
|
||||
# `refs/remotes` — e o que ela NÃO cobre (os PRs abertos pela lista do `gh`) sai
|
||||
# declarado na própria mensagem. Caso medido que CONTINUA fora: o 0269 do PR
|
||||
# aberto #965 (de fork) vivia só na cópia `refs/remotes/origin/pr/965`, e as
|
||||
# cópias `pr/N` ficam excluídas de propósito (sobrevivem ao fechamento do PR);
|
||||
# quem o pega é o `pnpm checar:colisao-de-migration`, pela lista de abertos. `scripts/migration-populacao.sh` é a mesma
|
||||
# regra que o `pnpm checar:colisao-de-migration` (o #1269) usa.
|
||||
BIBLIOTECA="$top/scripts/migration-populacao.sh"
|
||||
if [ -r "$BIBLIOTECA" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$BIBLIOTECA" || true
|
||||
fi
|
||||
base=""
|
||||
if declare -F pop_main_do_produto >/dev/null 2>&1; then
|
||||
base="$(pop_main_do_produto || true)"
|
||||
if [ -n "$base" ] && ! git rev-parse -q --verify "${base}^{commit}" >/dev/null 2>&1; then
|
||||
base=""
|
||||
fi
|
||||
fi
|
||||
if [ -z "$base" ]; then
|
||||
base="origin/main"
|
||||
git rev-parse -q --verify "${base}^{commit}" >/dev/null 2>&1 || base=""
|
||||
fi
|
||||
if declare -F pop_refs_de_outrem >/dev/null 2>&1; then
|
||||
refs="$(pop_refs_de_outrem "$base" 2>/dev/null || true)"
|
||||
else
|
||||
refs="origin/main $(git for-each-ref --format='%(refname)' refs/heads refs/remotes 2>/dev/null || true)"
|
||||
fi
|
||||
# A lista que entrou é a POPULAÇÃO que o grep vai casar, e ela NUNCA pode ficar
|
||||
# vazia por acidente: um `pop_migrations` que sai vazio (biblioteca ausente, ou
|
||||
# clone em que nenhuma rev resolveu) transformaria o `grep` de baixo em "não há
|
||||
# colisão" e o hook LIBERARIA o commit. Um guard que encolhe o universo em
|
||||
# silêncio é o defeito que a #1273 corrige. A degradação é a de antes (medir as
|
||||
# refs que houver) com um aviso, nunca a população zerada.
|
||||
# `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=""
|
||||
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)"
|
||||
fi
|
||||
if [ -z "${populacao// /}" ] && [ -z "${populacao//$'\n'/}" ]; then
|
||||
if ! declare -F pop_migrations >/dev/null 2>&1; then
|
||||
fallback=""
|
||||
for ref in $refs HEAD; do
|
||||
[ -n "$ref" ] || continue
|
||||
arquivos="$(git ls-tree -r --name-only "$ref" -- supabase/migrations 2>/dev/null \
|
||||
| sed 's#^supabase/migrations/##' || true)"
|
||||
# Prefixa CADA linha com a ref: o grep de baixo casa "<ref> <nome>", e
|
||||
# prefixar só a 1ª deixava passar colisão com qualquer arquivo que não
|
||||
# fosse o primeiro da ref.
|
||||
[ -n "$arquivos" ] && fallback="${fallback}$(awk -v r="$ref" '{ print r, $0 }' <<<"$arquivos")"$'\n'
|
||||
done
|
||||
populacao="$fallback"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sequência NNNN única contra a POPULAÇÃO inteira.
|
||||
while IFS= read -r path; do
|
||||
fname=$(basename "$path")
|
||||
nnnn=$(sed -nE 's/^[0-9]+_([0-9]{4})_.+\.sql$/\1/p' <<<"$fname")
|
||||
nnnn=$(sed -nE 's/^[0-9]{14}_([0-9]{4})_.+\.sql$/\1/p' <<<"$fname")
|
||||
if [ -z "$nnnn" ]; then
|
||||
echo "pre-commit BLOQUEADO: '$fname' não segue o padrão <timestamp>_<NNNN>_<slug>.sql do repo." >&2
|
||||
echo "pre-commit BLOQUEADO: '$fname' não segue o padrão <timestamp de 14 dígitos>_<NNNN>_<slug>.sql do repo." >&2
|
||||
exit 1
|
||||
fi
|
||||
while IFS= read -r branch; do
|
||||
conflict=$(git ls-tree -r --name-only "$branch" -- supabase/migrations 2>/dev/null \
|
||||
| grep -E "^supabase/migrations/[0-9]+_${nnnn}_.+\.sql$" || true)
|
||||
if [ -n "$conflict" ]; then
|
||||
echo "pre-commit BLOQUEADO: sequência NNNN=$nnnn de '$fname' já existe na branch '$branch':" >&2
|
||||
echo " $conflict" >&2
|
||||
echo "Escolha o próximo NNNN livre em TODAS as branches locais (git branch --format='%(refname:short)' + git ls-tree)." >&2
|
||||
echo "E troque o TIMESTAMP JUNTO: renumerar só o NNNN fabricou 12 das colisões de timestamp deste repo." >&2
|
||||
echo "Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2
|
||||
houve_conflito=1
|
||||
break
|
||||
# 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" \
|
||||
| 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
|
||||
else
|
||||
echo "Para o próximo número livre (main do produto ∪ PRs abertos): pnpm checar:colisao-de-migration" >&2
|
||||
fi
|
||||
done < <(git branch --format='%(refname:short)')
|
||||
echo "E troque o TIMESTAMP JUNTO: renumerar só o NNNN fabricou 12 das colisões de timestamp deste repo." >&2
|
||||
echo "Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2
|
||||
houve_conflito=1
|
||||
fi
|
||||
done <<<"$new_migrations"
|
||||
|
||||
|
||||
@@ -113,26 +185,24 @@ DIVIDA_DE_TIMESTAMP="
|
||||
20260825120000 20260826190000 20260827010000
|
||||
"
|
||||
|
||||
# Mesma POPULAÇÃO do NNNN, acima — a resposta à pergunta do timestamp é a mesma
|
||||
# resposta sobre o mesmo conjunto. A âncora é a posição do nome canônico, e não
|
||||
# `^supabase/migrations/…`: a lista de `pop_migrations` é "<ref> <nome>".
|
||||
while IFS= read -r path; do
|
||||
fname=$(basename "$path")
|
||||
ts=$(sed -nE 's/^([0-9]{14})_[0-9]{4}_.+\.sql$/\1/p' <<<"$fname")
|
||||
[ -z "$ts" ] && continue
|
||||
grep -qw "$ts" <<<"$DIVIDA_DE_TIMESTAMP" && continue
|
||||
|
||||
while IFS= read -r branch; do
|
||||
conflict=$(git ls-tree -r --name-only "$branch" -- supabase/migrations 2>/dev/null \
|
||||
| grep -E "^supabase/migrations/${ts}_[0-9]{4}_.+\.sql$" \
|
||||
| grep -v "^supabase/migrations/${fname}$" || true)
|
||||
if [ -n "$conflict" ]; then
|
||||
echo "pre-commit BLOQUEADO: o TIMESTAMP $ts de '$fname' já existe na branch '$branch':" >&2
|
||||
echo " $conflict" >&2
|
||||
echo "O timestamp é a PK de supabase_migrations.schema_migrations: repetido, o db push colide na PK e o db reset quebra (issue #143)." >&2
|
||||
echo "Escolha um instante livre — e renumerar só o NNNN não resolve: os dois têm de ser únicos." >&2
|
||||
echo "Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2
|
||||
houve_conflito=1
|
||||
break
|
||||
fi
|
||||
done < <(git branch --format='%(refname:short)')
|
||||
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
|
||||
echo "O timestamp é a PK de supabase_migrations.schema_migrations: repetido, o db push colide na PK e o db reset quebra (issue #143)." >&2
|
||||
echo "Escolha um instante livre — e renumerar só o NNNN não resolve: os dois têm de ser únicos." >&2
|
||||
echo "Correção orientada pelo dono: DESKCOMM_GOV_MIGRATION_EDIT=1." >&2
|
||||
houve_conflito=1
|
||||
fi
|
||||
done <<<"$new_migrations"
|
||||
|
||||
[ "$houve_conflito" = 1 ] && exit 1
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env bash
|
||||
# migration-populacao.sh — a POPULAÇÃO da pergunta "esse número de migration já
|
||||
# foi usado ou reservado?" (issue #1273).
|
||||
#
|
||||
# ## Por que isto é uma biblioteca, e não mais uma cópia
|
||||
#
|
||||
# Oito lugares do repo respondiam a essa pergunta, e cada um media um recorte
|
||||
# menor que o da pergunta (issue #1273): a main de um FORK em vez da main do
|
||||
# produto, só branches locais sem os PRs abertos, cabeças já buscadas de PRs
|
||||
# FECHADOS, o DISCO em vez da main atual, e uma cópia VELHA da cabeça de um PR.
|
||||
# Cada recorte era uma reimplementação da mesma função — e uma regra que diverge
|
||||
# oito vezes é a assinatura de que ela precisa de UM lugar só.
|
||||
#
|
||||
# A referência do padrão é `scripts/checar-colisao-de-migration.sh` (o #1269):
|
||||
# a mesma âncora de NNNN, a mesma lista de PRs ABERTOS (nunca o curinga
|
||||
# refs/pull/*, que persiste depois do fechamento), e o mesmo "NÃO MEDIDO"
|
||||
# declarado em vez de silêncio. Este arquivo é a parte que hooks locais e
|
||||
# pré-voo podem usar sem ir à rede; o script de CI, que pode ir, continua
|
||||
# medindo também as cabeças dos PRs abertos.
|
||||
#
|
||||
# ## Uso
|
||||
#
|
||||
# source scripts/migration-populacao.sh
|
||||
# ref="$(pop_main_do_produto)" # a main do PRODUTO, ou nada + rc 1
|
||||
# pop_refs_de_outrem "$ref" # a população local de refs
|
||||
# pop_migrations "$ref" $(pop_refs_de_outrem "$ref") # "<ref> <arquivo>"
|
||||
# ... | pop_nnnn_de # só o NNNN, com a âncora certa
|
||||
#
|
||||
# ## Ausência da biblioteca: degrada, não quebra
|
||||
#
|
||||
# Quem chama (hooks de pre-commit, pré-voo) faz `source` com `|| true` e, sem
|
||||
# este arquivo, mantém a população antiga e DIZ que é a antiga. Um hook que
|
||||
# encolhe o universo sem dizer é o defeito que esta issue corrige; então a
|
||||
# ausência nunca é silenciosa, e nunca vira bloqueio novo: sem a biblioteca o
|
||||
# comportamento é byte-a-byte o de antes, mais uma linha declarando.
|
||||
#
|
||||
# O `guard-hooks-atualizados.sh` (primeiro do dispatcher) compara `loop/hooks/`
|
||||
# com a `origin/main`; esta biblioteca mora fora dele porque ela não é um hook.
|
||||
# Isso é seguro pelo motivo declarado: hook e biblioteca viajam no MESMO
|
||||
# commit, então um checkout nunca tem o hook novo com a biblioteca velha — e
|
||||
# quando falta a biblioteca, o caminho de declaração é o de antes.
|
||||
#
|
||||
# ## O que aqui NÃO cabe, e por quê
|
||||
#
|
||||
# Os PRs ABERTOS não entram: hooks de pre-commit não vão à rede. O que este
|
||||
# arquivo faz é entregar a metade que é LOCAL e nomear o que ficou de fora,
|
||||
# com o comando que cobre o resto (`pnpm checar:colisao-de-migration`). Um hook
|
||||
# que trava commit por falta de rede troca um risco raro por um travamento
|
||||
# diário — o mesmo critério do `guard-hooks-atualizados.sh` (§"O que este
|
||||
# guard NÃO faz, declarado").
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# pop_slug_de <url> — "dono/repo" em minúsculas, sem esquema/host e sem ".git".
|
||||
# A URL vem CRUA de `git config --get remote.<n>.url`: `git remote get-url` já
|
||||
# aplica o insteadOf, e o insteadOf é justamente o que faz um fork parecer o
|
||||
# principal. (ssh `git@github.com:dono/repo.git` e https caem nos dois mesmos.)
|
||||
pop_slug_de() {
|
||||
printf '%s' "${1:-}" | tr 'A-Z' 'a-z' \
|
||||
| sed -E 's#^.*github\.com[:/]+##; s#\.git$##; s#/+$##'
|
||||
}
|
||||
|
||||
# pop_main_do_produto — a ref que é a main do REPOSITÓRIO PRINCIPAL.
|
||||
#
|
||||
# A pergunta "este número já foi usado?" é sobre o PRODUTO, não sobre o seu
|
||||
# clone: num fork, a `origin/main` é a main do fork, que pode estar atrás da do
|
||||
# principal (medido no caso da #1273: fork parado em 0323, principal já em
|
||||
# 0324, e o hook dizia "livre" com exit 0).
|
||||
#
|
||||
# O remoto que aponta para o repositório principal vale com QUALQUER nome
|
||||
# (`origin`, `upstream`, `produto`) — é a URL que decide, não o nome.
|
||||
#
|
||||
# Sai 1 (e imprime nada) quando nenhum remoto é o principal. O chamador
|
||||
# ESCOLHE o que fazer: quem só tem remotos de fork declara NÃO MEDIDO; quem
|
||||
# está em fixture sem rede nenhuma (nenhum remoto de GitHub) continua medindo o
|
||||
# que já media.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
pop_main_do_produto() {
|
||||
local r url slug
|
||||
for r in $(git remote 2>/dev/null); do
|
||||
url="$(git config --get "remote.$r.url" 2>/dev/null || true)"
|
||||
[ -n "$url" ] || continue
|
||||
slug="$(pop_slug_de "$url")"
|
||||
# Os DOIS padrões: o slug exato, e o slug com algum caminho antes dele (uma
|
||||
# URL com host/prefixo que o `sed` não tirou). Só o de `*/` NÃO casa o slug
|
||||
# exato — `*/x` exige a barra — e isso devolvia vazio num clone cujo remoto
|
||||
# do produto estava certinho.
|
||||
case "$slug" in
|
||||
melgarafael/deskcommcrm | */melgarafael/deskcommcrm)
|
||||
printf '%s/main\n' "$r"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# pop_repo_do_produto — "dono/repo" do principal, para o `gh` (`--repo`).
|
||||
# As mesmas duas formas de slug do `pop_main_do_produto`, pelo mesmo motivo.
|
||||
pop_repo_do_produto() {
|
||||
local r url slug
|
||||
for r in $(git remote 2>/dev/null); do
|
||||
url="$(git config --get "remote.$r.url" 2>/dev/null || true)"
|
||||
[ -n "$url" ] || continue
|
||||
slug="$(pop_slug_de "$url")"
|
||||
case "$slug" in
|
||||
melgarafael/deskcommcrm | */melgarafael/deskcommcrm)
|
||||
printf 'melgarafael/DeskcommCRM\n'; return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# pop_tem_remoto_de_github — existe algum remoto de GitHub (mesmo de fork)?
|
||||
# Serve para distinguir "clone de fork sem o remoto do principal" (que precisa
|
||||
# declarar NÃO MEDIDO) de "fixture sem nenhum remoto" (que só tem o que tem).
|
||||
pop_tem_remoto_de_github() {
|
||||
local url
|
||||
for url in $(git remote 2>/dev/null); do
|
||||
case "$(git config --get "remote.$url.url" 2>/dev/null || true)" in
|
||||
*github.com*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# pop_refs_de_outrem <base> — as refs cujas migrations entram na conta local.
|
||||
#
|
||||
# * a BASE entra sempre que resolver: é a main do produto.
|
||||
# * `refs/heads` e `refs/remotes` entram JUNTAS. Só as locais (o
|
||||
# `git branch` de antes) deixava de fora a branch que só existe no remoto
|
||||
# (a de outra máquina, a do fork de um colega).
|
||||
# * as cópias de PR em `refs/remotes/[<remoto>/]pr/N` ficam DE FORA (as duas
|
||||
# formas: a triagem busca em `refs/remotes/pr/N`, 1336 no clone do
|
||||
# mantenedor em 27/09/2026). Por isso o 0269 do #965, que vivia só na
|
||||
# cópia `refs/remotes/origin/pr/965`, NÃO entra aqui: elas são cópias
|
||||
# de cabeças, não PRs abertos, e sobrevivem ao fechamento do PR (medido no
|
||||
# clone do mantenedor em 19/09/2026: 965 cabeças contra 43 PRs abertos).
|
||||
# Medi-las traria de volta, pela porta dos fundos, o número de um PR morto
|
||||
# — o que é exatamente o defeito que o #1269 fechou no script de CI.
|
||||
# * `refs/remotes/*/HEAD` é ponteiro de conveniência, nunca um dono.
|
||||
# * a ref que resolve para o MESMO commit da base (já medida) ou do HEAD (o
|
||||
# PR de quem roda) sai da conta, senão a varredura acusa o autor de colidir
|
||||
# com a própria branch (armadilha da #1155).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
pop_refs_de_outrem() {
|
||||
local base="${1:-}"
|
||||
local bc hc
|
||||
bc="$(git rev-parse -q --verify "${base}^{commit}" 2>/dev/null || true)"
|
||||
hc="$(git rev-parse -q --verify "HEAD^{commit}" 2>/dev/null || true)"
|
||||
git for-each-ref --format='%(objectname) %(refname)' refs/heads refs/remotes 2>/dev/null \
|
||||
| awk -v b="$bc" -v h="$hc" '
|
||||
$1 != b && $1 != h && $2 !~ /\/HEAD$/ \
|
||||
&& $2 !~ /^refs\/remotes\/(.+\/)?pr\/[0-9]+$/ { print $2 }'
|
||||
[ -n "$base" ] || return 0
|
||||
git rev-parse -q --verify "${base}^{commit}" >/dev/null 2>&1 && printf '%s\n' "$base"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# pop_migrations <rev>... — "<rev> <nome do arquivo>", a lista INTEIRA de
|
||||
# supabase/migrations de cada rev, em três processos para quantas revs houver.
|
||||
#
|
||||
# Por que não um `git ls-tree` por ref: no clone do mantenedor (188 heads + 337
|
||||
# remotes) são 525 processos, e o `git branch --format=… | while` do hook antigo
|
||||
# reescrevia a lista a cada volta. Aqui: `cat-file --batch-check` resolve as
|
||||
# árvores de supabase/migrations de TODAS as revs numa chamada; as árvores
|
||||
# ÚNICAS são listadas por um `diff-tree` contra a árvore vazia, em outra; e o
|
||||
# awk cola cada rev à sua árvore. Revs que são o MESMO commit (mesma árvore)
|
||||
# compartilham a listagem — que é o caso comum num clone com muitas branches.
|
||||
#
|
||||
# Como o git resolve QUALQUER revisão, `HEAD` e a base entram na mesma chamada,
|
||||
# sem caso especial.
|
||||
#
|
||||
# Rev que não resolve, ou que não tem supabase/migrations, responde `missing` e
|
||||
# não aparece. Isso é "não há o que medir" (a árvore não existe), e não o NÃO
|
||||
# MEDIDO de uma cabeça de PR que não pôde ser buscada — esse caso é do script de
|
||||
# CI, que NOMEIA o PR em vez de pulá-lo calado.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
pop_migrations() {
|
||||
[ "$#" -gt 0 ] || return 0
|
||||
local tmp vazia
|
||||
tmp="$(mktemp 2>/dev/null)" || return 0
|
||||
# "<rev>:supabase/migrations" → a ÁRVORE de cada rev, num processo só. O `paste`
|
||||
# cola a lista de revs na resposta, que o `cat-file` não sabe fazer sozinho.
|
||||
printf '%s:supabase/migrations\n' "$@" \
|
||||
| git cat-file --batch-check='%(objectname) %(objecttype)' 2>/dev/null \
|
||||
| paste -d' ' <(printf '%s\n' "$@") - \
|
||||
| awk '$3 == "tree" { print $1, $2 }' > "$tmp" || true
|
||||
[ -s "$tmp" ] || { rm -f "$tmp"; return 0; }
|
||||
# As árvores ÚNICAS contra a árvore VAZIA listam cada árvore inteira, e
|
||||
# `diff-tree --stdin` faz isso num processo para quantas forem. Revs que são o
|
||||
# MESMO commit compartilham a listagem — que é o caso comum num clone com
|
||||
# muita branch. Este é o mesmo caminho do #1269 (`nomes_por_arvore`), que foi
|
||||
# medido no clone do mantenedor (525 refs): um `ls-tree` por ref eram 525
|
||||
# processos, e a lista era reescrita a cada volta do `while`.
|
||||
vazia="$(git hash-object -t tree /dev/null)"
|
||||
cut -d' ' -f2 "$tmp" | sort -u | sed "s#^#$vazia #" \
|
||||
| git diff-tree -r --name-only --stdin 2>/dev/null \
|
||||
| awk -v v="$vazia" '$1 == v && NF == 2 { a = $2; next } { print a, $0 }' > "$tmp.nomes"
|
||||
# "$tmp" = "<rev> <árvore>"; "$tmp.nomes" = "<árvore> <nome>", uma linha por
|
||||
# arquivo. A junção é pela ÁRVORE (é o que o diff-tree devolveu) e a SAÍDA sai
|
||||
# em DOIS campos "<rev> <nome>" — quem consome chega ao nome com
|
||||
# `cut -d' ' -f2-`, e um terceiro campo viraria parte do nome dele.
|
||||
awk 'NR == FNR { arvores[$1] = arvores[$1] (arvores[$1] ? "\n" : "") $2; next }
|
||||
($2 in arvores) { m = split(arvores[$2], l, "\n");
|
||||
for (i = 1; i <= m; i++) if (l[i] != "") print $1, l[i] }' \
|
||||
"$tmp.nomes" "$tmp"
|
||||
rm -f "$tmp" "$tmp.nomes"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# pop_nnnn_de — stdin com NOMES de migration (com ou sem pasta, e com ou sem o
|
||||
# "<rev> " na frente, como sai de `pop_migrations`), stdout só com os NNNN, na
|
||||
# posição certa do nome canônico. O `cut -f2-` é o que faz as duas formas
|
||||
# entrarem: com nome solto ele é identidade.
|
||||
#
|
||||
# A âncora `^[0-9]{14}_([0-9]{4})_` é a mesma do #1269 (`nnnn_das`) e o mesmo
|
||||
# motivo: `grep -oE '_[0-9]{4}_'` e `sed -E 's/.*_([0-9]{4})_.*/\1/'` pegam
|
||||
# qualquer grupo de 4 dígitos entre `_`, inclusive um que esteja no SLUG. Com
|
||||
# `…_0326_relatorio_2024_anual.sql` a extração solta dá teto 2024 e esconde
|
||||
# uma duplicata real de 0326; e o `s#.*/#` antes do `sed` é obrigatório porque
|
||||
# o caminho com pasta (`supabase/migrations/…`) não casa a âncora (medido: 0
|
||||
# linhas de saída).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
pop_nnnn_de() { pop_campos | sed 's#.*/##' | sed -nE 's/^[0-9]{14}_([0-9]{4})_.*$/\1/p'; }
|
||||
|
||||
# pop_campos — stdin no formato de `pop_migrations` ("<rev> <nome>"), stdout só
|
||||
# com os NOMES. Sem ele, todo teste de colisão teria de lembrar de tirar a
|
||||
# ref da frente, e um `^` de âncora casaria a linha errada em silêncio.
|
||||
pop_campos() { cut -d' ' -f2-; }
|
||||
|
||||
# pop_teto_da_populacao — o MAIOR NNNN já usado na população que entrou.
|
||||
pop_teto_da_populacao() { pop_nnnn_de | sort -n | tail -1; }
|
||||
|
||||
# pop_tem_nnnn <nnnn> — o NNNN já está em uso na população? Sai 0 se sim.
|
||||
# Entrada: a lista de `pop_migrations` ("<ref> <nome>"). O padrão casa o NNNN na
|
||||
# POSIÇÃO do nome canônico e não qualquer "_NNNN_": é a mesma âncora de
|
||||
# `pop_nnnn_de`, e é ela que impede 0400 de casar em `…_0400_` e também
|
||||
# `0400` de casar no meio de `_10400_`.
|
||||
pop_tem_nnnn() { pop_campos | grep -qE "[0-9]{14}_${1}_"; }
|
||||
|
||||
# pop_ts_em_uso <timestamp> — o instantâneo já está em uso? Mesma posição, outro
|
||||
# campo: o Supabase usa o timestamp como identidade da migration.
|
||||
pop_ts_em_uso() { pop_campos | grep -qE "^${1}_[0-9]{4}_"; }
|
||||
|
||||
# pop_dica_proximo_livre <nnnn> <base> <populacao> — a dica de renumeração.
|
||||
#
|
||||
# A dica anterior ("Próximo livre: … | tail -1") era DUAS coisas erradas de uma
|
||||
# vez: devolvia o MAIOR número já usado (não o próximo livre — 0335, que já
|
||||
# existia na main, era declarado "livre"), e media só a `origin/main` com um `sed`
|
||||
# guloso que pegava o número do slug. Agora devolve o MAIOR + 1, contado sobre a
|
||||
# MESMA população que o hook confere, e diz sobre qual população foi.
|
||||
pop_dica_proximo_livre() { # $1 = o NNNN em colisão, $2 = base, $3 = população
|
||||
local colidindo="$1" base="${2:-}" pop="${3:-}"
|
||||
local teto proximo
|
||||
teto="$(pop_nnnn_de <<<"$pop" | sort -n | tail -1)"
|
||||
if [ -z "$teto" ]; then
|
||||
echo " Nenhuma migration com NNNN entrou na população medida; troque o NNNN e o timestamp e meça de novo."
|
||||
else
|
||||
proximo="$(printf '%04d' $((10#$teto + 1)))"
|
||||
echo " Teto medido sobre a população do próprio hook${base:+ (base '$base')}: NNNN=${teto} → próximo livre ${proximo}."
|
||||
[ "$proximo" = "$colidindo" ] && \
|
||||
echo " (o número que você tentou é o próximo livre da população; o que está tomado é o de outra branch ou de um PR aberto)"
|
||||
fi
|
||||
pop_aviso_populacao "$base" | sed 's/^/ /'
|
||||
}
|
||||
|
||||
# pop_aviso_populacao — a linha que DECLARA o que a conta não cobriu.
|
||||
# População sem escopo declarado é o defeito que a #1155 registrou: número sem
|
||||
# régua declarada é número que alguém renumera errado.
|
||||
pop_aviso_populacao() { # $1 = "base" ou vazio; $2 = nome da base
|
||||
local base="${1:-}" nome="${2:-origin/main}"
|
||||
if [ -z "$base" ]; then
|
||||
printf 'NÃO MEDIDO: a main do PRODUTO (remoto que aponta para melgarafael/DeskcommCRM) não está neste clone.\n'
|
||||
fi
|
||||
printf 'PRs ABERTOS (inclusive de fork) NÃO foram medidos aqui: hook e pré-voo não vão à rede.\n'
|
||||
printf ' Para medir também os PRs abertos: pnpm checar:colisao-de-migration (roda depois do commit).\n'
|
||||
}
|
||||
@@ -11,6 +11,14 @@
|
||||
# 2. check-migration-triple.sh BLOQUEIA migration nova sem baseline/MANIFEST,
|
||||
# BLOQUEIA NNNN e timestamp já usados na origin/main, e DEIXA PASSAR a
|
||||
# tripla completa com número livre. Bypass DESKCOMM_MIGRATION_EDIT=1.
|
||||
# 2-bis. e, com a biblioteca da população AUSENTE (#1273), o NNNN já usado
|
||||
# CONTINUA bloqueado e a queda é declarada: o guard não pode afrouxar
|
||||
# porque a regra que ele consulta não estava à mão.
|
||||
# 2-quater. o merge da main do PRODUTO passa mesmo com o NNNN dela noutra
|
||||
# branch do principal; um NNNN novo da branch continua bloqueado.
|
||||
# 2-quinquies. o NNNN e o timestamp já COMMITADOS na própria branch contam:
|
||||
# a segunda 0201 e o carimbo repetido são bloqueados, e a dica não aponta
|
||||
# para o número que a branch já usa.
|
||||
# 3. pre-push BLOQUEIA refs/heads/main e deixa passar uma feature branch.
|
||||
# 4. armar-hooks.sh grava core.hooksPath, recusa sobrescrever hooks alheios,
|
||||
# e --desarmar limpa.
|
||||
@@ -22,7 +30,8 @@
|
||||
# num clone sem o script e fora de qualquer clone, com e sem a instalação global.
|
||||
set -uo pipefail
|
||||
|
||||
SKILL="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.agents/skills/deskcomm-contribuir" && pwd)"
|
||||
RAIZ="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SKILL="$RAIZ/.agents/skills/deskcomm-contribuir"
|
||||
SCRIPTS="$SKILL/scripts"
|
||||
falhas=0; casos=0
|
||||
ok() { casos=$((casos+1)); printf ' ✓ %s\n' "$1"; }
|
||||
@@ -60,6 +69,11 @@ git -C "$principal" init -q -b main
|
||||
cfg "$principal" user.email "mantenedor@exemplo.com"; cfg "$principal" user.name "Mantenedor"
|
||||
mkdir -p "$principal/supabase/migrations" "$principal/.agents/skills/deskcomm-contribuir/scripts/hooks"
|
||||
cp -R "$SCRIPTS"/. "$principal/.agents/skills/deskcomm-contribuir/scripts/"
|
||||
# A biblioteca da POPULAÇÃO (#1273) mora em scripts/, fora da skill — e sem ela
|
||||
# os hooks caem no caminho degradado e medem menos. O caso 2 mede o caminho
|
||||
# COM a biblioteca; o 2-bis apaga o arquivo de propósito e mede o sem.
|
||||
mkdir -p "$principal/scripts"
|
||||
cp "$RAIZ/scripts/migration-populacao.sh" "$principal/scripts/"
|
||||
printf 'select 1;\n' > "$principal/supabase/migrations/20260101120000_0200_existente.sql"
|
||||
printf -- '-- baseline\n' > "$principal/supabase/baseline.sql"
|
||||
printf '| `20260101120000` | `0200_existente` |\n' > "$principal/supabase/migrations/MANIFEST.md"
|
||||
@@ -113,6 +127,147 @@ saida="$(DESKCOMM_MIGRATION_EDIT=1 git commit -q -m "bypass" 2>&1)"; code=$?
|
||||
assert_exit "$code" 0 "DESKCOMM_MIGRATION_EDIT=1 é o bypass explícito"
|
||||
git reset -q --hard HEAD~1 2>/dev/null
|
||||
|
||||
# ── 2-bis. A AUSÊNCIA da biblioteca NÃO afrouxa o guard (#1273) ─────────────────
|
||||
# O caso que a issue descreve: a regra da população vive num lugar só
|
||||
# (scripts/migration-populacao.sh), e um clone antigo, ou uma cópia da skill de
|
||||
# uma versão anterior, NÃO tem esse arquivo. Se a queda da biblioteca esvaziasse
|
||||
# a população, o `grep` não acharia colisão nenhuma e o pre-commit LIBERARIA o
|
||||
# commit — um guard que encolhe o universo em silêncio é exatamente o defeito
|
||||
# que a #1273 corrige. Aqui a biblioteca é apagada DE VERDADE do clone e o NNNN
|
||||
# já usado na origin/main tem de continuar bloqueado, com o aviso da degradação.
|
||||
echo "2-bis. check-migration-triple.sh sem a biblioteca (a guarda NÃO afrouxa)"
|
||||
sem_lib="$TMP/c2-sem-lib"
|
||||
rm -rf "$sem_lib"; git clone -q "$principal" "$sem_lib"
|
||||
cfg "$sem_lib" user.email "alguem@fork.dev"; cfg "$sem_lib" user.name "Pessoa"
|
||||
cfg "$sem_lib" core.hooksPath ".agents/skills/deskcomm-contribuir/scripts/hooks"
|
||||
chmod +x "$sem_lib"/.agents/skills/deskcomm-contribuir/scripts/*.sh \
|
||||
"$sem_lib"/.agents/skills/deskcomm-contribuir/scripts/hooks/*
|
||||
cd "$sem_lib" || exit 1; git switch -q -c fix/sem-lib
|
||||
rm -f scripts/migration-populacao.sh # a queda real da biblioteca
|
||||
[ -r scripts/migration-populacao.sh ] && falha "2-bis" "a biblioteca não saiu do clone: o caso não mede a queda"
|
||||
printf 'select 3;\n' > supabase/migrations/20260101120000_0200_colide.sql
|
||||
printf -- '-- x\n' >> supabase/baseline.sql
|
||||
printf '| x | `0200_colide` |\n' >> supabase/migrations/MANIFEST.md
|
||||
git add -A
|
||||
saida="$(git commit -q -m "colisao sem biblioteca" 2>&1)"; code=$?
|
||||
assert_exit "$code" 1 "sem a biblioteca, o NNNN já usado AINDA é bloqueado (o guard não afrouxa)"
|
||||
assert_contains "$saida" "NNNN=0200" "sem a biblioteca, o NNNN continua sendo acusado"
|
||||
assert_contains "$saida" "migration-populacao.sh AUSENTE" "a queda da biblioteca é DECLARADA, não silenciosa"
|
||||
assert_contains "$saida" "checar:colisao-de-migration" "o aviso aponta quem mede a população inteira"
|
||||
assert_not_contains "$saida" "command not found" "sem a biblioteca, o bloqueio não tropeça em função ausente"
|
||||
# O NNNN em colisão que NÃO é o 1º arquivo da ref: o caminho sem biblioteca
|
||||
# prefixava só a 1ª linha de cada ref, e o `grep "<ref> <nome>"` perdia o resto.
|
||||
git reset -q --hard HEAD 2>/dev/null
|
||||
git switch -q -c colega
|
||||
printf 'select 5;\n' > supabase/migrations/20260301120000_0300_do_colega.sql
|
||||
printf 'select 6;\n' > supabase/migrations/20260301130000_0301_do_colega.sql
|
||||
git add -A; DESKCOMM_MIGRATION_EDIT=1 git commit -q -m "colega"
|
||||
git switch -q fix/sem-lib
|
||||
rm -f scripts/migration-populacao.sh
|
||||
printf 'select 7;\n' > supabase/migrations/20260909120000_0301_colide.sql
|
||||
printf -- '-- y\n' >> supabase/baseline.sql
|
||||
printf '| y | `0301_colide` |\n' >> supabase/migrations/MANIFEST.md
|
||||
git add -A
|
||||
saida="$(git commit -q -m "colisao com arquivo que nao e o primeiro da ref" 2>&1)"; code=$?
|
||||
assert_exit "$code" 1 "sem a biblioteca, colisão com o 3º arquivo de outra branch AINDA é bloqueada"
|
||||
assert_contains "$saida" "NNNN=0301" "e o NNNN acusado é o do 3º arquivo"
|
||||
git reset -q --hard HEAD 2>/dev/null
|
||||
|
||||
# ── 2-ter. Clone que só tem o FORK (item 1 da #1273) ─────────────────────────
|
||||
# A `origin` aponta para um fork no GitHub (o insteadOf a resolve para o
|
||||
# principal local, sem rede) e nenhum remoto é melgarafael/DeskcommCRM: a base
|
||||
# vira a origin/main DO FORK, que pode estar atrás do principal. O hook não
|
||||
# bloqueia por isso, mas DIZ; e o pré-voo não dá ✓ de "livre" sobre essa régua.
|
||||
echo "2-ter. clone só com o fork: NÃO MEDIDO declarado"
|
||||
fork="$TMP/c2-fork"; clonar "$fork" "alguem@fork.dev"
|
||||
cfg "$fork" remote.origin.url "https://github.com/alguem/DeskcommCRM.git"
|
||||
cfg "$fork" "url.$principal.insteadOf" "https://github.com/alguem/DeskcommCRM.git"
|
||||
cd "$fork" || exit 1; git switch -q -c fix/no-fork
|
||||
printf 'select 8;\n' > supabase/migrations/20260909130000_0201_no_fork.sql
|
||||
printf -- '-- z\n' >> supabase/baseline.sql
|
||||
printf '| z | `0201_no_fork` |\n' >> supabase/migrations/MANIFEST.md
|
||||
git add -A
|
||||
saida="$(git commit -q -m "migration num fork" 2>&1)"; code=$?
|
||||
assert_exit "$code" 0 "número livre no fork não bloqueia (não há colisão medida)"
|
||||
assert_contains "$saida" "NÃO MEDIDOS contra a main do PRODUTO" "o hook declara que não mediu a main do produto"
|
||||
assert_contains "$saida" "git remote add upstream" "e diz como corrigir"
|
||||
saida="$(bash .agents/skills/deskcomm-contribuir/scripts/pre-voo.sh 2>&1)"
|
||||
assert_not_contains "$saida" "✓ NNNN" "o pré-voo não dá ✓ de NNNN livre sobre a main do fork"
|
||||
assert_not_contains "$saida" "✓ próximo NNNN" "nem ✓ de próximo NNNN"
|
||||
assert_contains "$saida" "NÃO MEDIDO contra a main do PRODUTO" "o pré-voo declara a régua"
|
||||
|
||||
# ── 2-quater. Trazer a main do PRODUTO não é "migration nova" ─────────────────
|
||||
# Com a população alargada para refs/remotes/*, uma branch do principal com OUTRO
|
||||
# arquivo de mesmo NNNN (o 0412 de `resgate/1651-…` contra o 0412 da main, medido
|
||||
# em 27/09/2026) fazia o merge da upstream/main ser BLOQUEADO, mandando renumerar
|
||||
# migration que já está na main. O que a main do produto já tem sai da conta.
|
||||
echo "2-quater. merge da main do produto não é bloqueado por branch velha do principal"
|
||||
produto="$TMP/produto"; rm -rf "$produto"; git clone -q "$principal" "$produto"
|
||||
cfg "$produto" user.email "mantenedor@exemplo.com"; cfg "$produto" user.name "Mantenedor"
|
||||
git -C "$produto" switch -q -c velha
|
||||
printf 'select 9;\n' > "$produto/supabase/migrations/20260102000000_0412_versao_antiga.sql"
|
||||
git -C "$produto" add -A; git -C "$produto" commit -q -m "velha"
|
||||
git -C "$produto" switch -q main
|
||||
printf 'select 10;\n' > "$produto/supabase/migrations/20260103000000_0412_versao_da_main.sql"
|
||||
printf -- '-- apêndice 0412\n' >> "$produto/supabase/baseline.sql"
|
||||
printf '| `20260103000000` | `0412_versao_da_main` |\n' >> "$produto/supabase/migrations/MANIFEST.md"
|
||||
git -C "$produto" add -A; git -C "$produto" commit -q -m "0412 na main"
|
||||
merge="$TMP/c2-merge"; clonar "$merge" "alguem@fork.dev"
|
||||
cfg "$merge" remote.upstream.url "https://github.com/melgarafael/DeskcommCRM.git"
|
||||
cfg "$merge" remote.upstream.fetch "+refs/heads/*:refs/remotes/upstream/*"
|
||||
cfg "$merge" "url.$produto.insteadOf" "https://github.com/melgarafael/DeskcommCRM.git"
|
||||
cd "$merge" || exit 1; git fetch -q upstream; git switch -q -c fix/traz-a-main
|
||||
git rev-parse -q --verify refs/remotes/upstream/velha >/dev/null || falha "2-quater" "a branch velha do principal não chegou: o caso não mede a colisão"
|
||||
git merge -q --no-ff --no-commit upstream/main >/dev/null 2>&1
|
||||
saida="$(git commit -q -m "traz a main do produto" 2>&1)"; code=$?
|
||||
assert_exit "$code" 0 "o merge da main do produto passa, mesmo com 0412 noutra branch do principal"
|
||||
assert_not_contains "$saida" "BLOQUEADO" "e não manda renumerar migration que já está na main"
|
||||
# A guarda real segue inteira: um 0412 NOVO desta branch ainda colide.
|
||||
printf 'select 11;\n' > supabase/migrations/20260909140000_0412_minha.sql
|
||||
printf -- '-- w\n' >> supabase/baseline.sql; printf '| w | `0412_minha` |\n' >> supabase/migrations/MANIFEST.md
|
||||
git add -A
|
||||
saida="$(git commit -q -m "0412 meu" 2>&1)"; code=$?
|
||||
assert_exit "$code" 1 "um 0412 NOVO desta branch continua bloqueado"
|
||||
assert_contains "$saida" "upstream/main" "e o dono nomeado é a main do produto"
|
||||
git reset -q --hard HEAD 2>/dev/null
|
||||
|
||||
# ── 2-quinquies. O que a PRÓPRIA branch já commitou está na população ────────
|
||||
# `pop_refs_de_outrem` tira da conta a ref cujo SHA é o do HEAD (a #1155: não
|
||||
# acusar o autor de colidir consigo), e o hook não devolvia o HEAD — então a
|
||||
# 0201 que a branch JÁ commitou sumia da população: a segunda 0201 (e o mesmo
|
||||
# carimbo) passava calada, e a dica mandava renumerar para a 0201 da branch.
|
||||
echo "2-quinquies. a migration já commitada na própria branch conta"
|
||||
propria="$TMP/c2-propria"; clonar "$propria" "alguem@fork.dev"
|
||||
cd "$propria" || exit 1; git switch -q -c fix/duas
|
||||
tripla() { # $1 = nome em supabase/migrations/
|
||||
printf 'select 1;\n' > "supabase/migrations/$1"
|
||||
printf -- '-- apêndice %s\n' "$1" >> supabase/baseline.sql
|
||||
printf '| `%s` |\n' "$1" >> supabase/migrations/MANIFEST.md
|
||||
git add -A
|
||||
}
|
||||
tripla 20260910000000_0201_primeira.sql
|
||||
saida="$(git commit -q -m "0201 primeira" 2>&1)"; code=$?
|
||||
assert_exit "$code" 0 "a primeira 0201 da branch passa (número livre)"
|
||||
tripla 20260910010000_0201_segunda.sql
|
||||
saida="$(git commit -q -m "0201 segunda" 2>&1)"; code=$?
|
||||
assert_exit "$code" 1 "0201_primeira commitada + 0201_segunda encenada: BLOQUEIA"
|
||||
assert_contains "$saida" "já existe em: HEAD(20260910000000_0201_primeira.sql)" "e o dono nomeado é a própria branch"
|
||||
git reset -q --hard HEAD 2>/dev/null
|
||||
tripla 20260910000000_0202_mesmo_carimbo.sql
|
||||
saida="$(git commit -q -m "carimbo repetido" 2>&1)"; code=$?
|
||||
assert_exit "$code" 1 "carimbo da migration já commitada, repetido: BLOQUEIA"
|
||||
assert_contains "$saida" "timestamp 20260910000000 de '20260910000000_0202_mesmo_carimbo.sql' já existe em: HEAD" "e acusa o timestamp contra a própria branch"
|
||||
git reset -q --hard HEAD 2>/dev/null
|
||||
tripla 20260910020000_0200_da_main.sql
|
||||
saida="$(git commit -q -m "0200 da main de novo" 2>&1)"; code=$?
|
||||
assert_exit "$code" 1 "0200 da main encenada de novo segue bloqueada (controle positivo)"
|
||||
assert_contains "$saida" "próximo livre 0202" "a dica não manda para a 0201 que a branch já usa"
|
||||
git reset -q --hard HEAD 2>/dev/null
|
||||
tripla 20260910030000_0202_livre.sql
|
||||
saida="$(git commit -q -m "0202 livre" 2>&1)"; code=$?
|
||||
assert_exit "$code" 0 "e um número de fato livre passa (o HEAD não acusa o próprio arquivo)"
|
||||
cd "$merge" || exit 1
|
||||
|
||||
echo "3. pre-push"
|
||||
saida="$(printf 'refs/heads/fix/algo %s refs/heads/main %s\n' "$(git rev-parse HEAD)" "$(git rev-parse HEAD)" | bash .agents/skills/deskcomm-contribuir/scripts/hooks/pre-push origin x 2>&1)"; code=$?
|
||||
assert_exit "$code" 1 "push para refs/heads/main é bloqueado"
|
||||
|
||||
@@ -74,6 +74,10 @@
|
||||
# SIGPIPE, o `pipefail` propaga 141 e o `|| exit 0` engole — o hook saía 0 sem
|
||||
# validar NADA. Este caso é o que impede o conserto de trocar um defeito por outro.
|
||||
#
|
||||
# check-migration-triple.sh
|
||||
# 8. o NNNN e o timestamp já COMMITADOS na própria branch contam (MIG-PROPRIA), e a
|
||||
# guarda contra a main e contra outra branch segue de pé (MIG-CONTROLE).
|
||||
#
|
||||
# Controle de vivacidade: os casos 2, 3, 4, 5, 6 e 7 são as asserções POSITIVAS (A, B+, D,
|
||||
# D2, R1, R1-LIMPO, R-VELHO, FECHADO-SEM-BASE, FURO-A, FURO-A-MH, FURO-B, COLEGA-DEL, MODO,
|
||||
# CITADO, SEM-REF, F-A, F-B+, F-CRIA-PRÓPRIA, F-BIG+). Um hook substituído por `exit 0` os
|
||||
@@ -922,5 +926,55 @@ else falha "F-BIG+: volume >3000 e match na posição <10" "encenados=$encenados
|
||||
r=$(rodar "$fbig" validate-features.sh)
|
||||
assert_exit "$(exit_de "$r")" 1 "F-BIG+: em merge grande o hook AINDA valida — a sonda não falha aberta"
|
||||
|
||||
# ── check-migration-triple.sh · o que a PRÓPRIA branch já commitou conta (#1776) ──
|
||||
# `pop_refs_de_outrem` tira da conta a ref cujo SHA é o do HEAD, e o hook não devolvia
|
||||
# o HEAD: a 0411 que a branch JÁ commitou sumia da população, e a segunda 0411 (e o
|
||||
# carimbo repetido) passava calada. Na main de antes (`git branch`) ela bloqueava.
|
||||
printf '\ncheck-migration-triple.sh — a própria branch está na população\n'
|
||||
pmig="$TMP/pmig"; mkdir -p "$pmig/supabase/migrations"
|
||||
git -C "$pmig" init -q -b main
|
||||
printf 'select 1;\n' > "$pmig/supabase/migrations/20260801000000_0410_da_main.sql"
|
||||
printf -- '-- baseline\n' > "$pmig/supabase/baseline.sql"
|
||||
printf '| 0410 |\n' > "$pmig/supabase/migrations/MANIFEST.md"
|
||||
commitar "$pmig" "0410 na main"
|
||||
git -C "$pmig" checkout -q -b colega
|
||||
printf 'select 1;\n' > "$pmig/supabase/migrations/20260802000000_0413_do_colega.sql"
|
||||
commitar "$pmig" "0413 do colega"
|
||||
git -C "$pmig" checkout -q main
|
||||
m="$TMP/m-propria"; preparar "$m" "$pmig" main
|
||||
mkdir -p "$m/scripts"; cp "$RAIZ/scripts/migration-populacao.sh" "$m/scripts/"
|
||||
tripla() { # $1 = clone, $2 = nome em supabase/migrations/
|
||||
printf 'select 1;\n' > "$1/supabase/migrations/$2"
|
||||
printf -- '-- apêndice %s\n' "$2" >> "$1/supabase/baseline.sql"
|
||||
printf '| `%s` |\n' "$2" >> "$1/supabase/migrations/MANIFEST.md"
|
||||
git -C "$1" add -A
|
||||
}
|
||||
tripla "$m" 20260910000000_0411_primeira.sql
|
||||
r=$(rodar "$m" check-migration-triple.sh)
|
||||
assert_exit "$(exit_de "$r")" 0 "MIG-PROPRIA: a primeira 0411 da branch passa (número livre)"
|
||||
commitar "$m" "0411 primeira"
|
||||
tripla "$m" 20260910010000_0411_segunda.sql
|
||||
r=$(rodar "$m" check-migration-triple.sh)
|
||||
assert_exit "$(exit_de "$r")" 1 "MIG-PROPRIA: 0411_primeira commitada + 0411_segunda encenada: BLOQUEIA"
|
||||
assert_contains "$(saida_de "$r")" "já existe em: HEAD(20260910000000_0411_primeira.sql)" "MIG-PROPRIA: e o dono nomeado é a própria branch"
|
||||
git -C "$m" reset -q --hard HEAD
|
||||
tripla "$m" 20260910000000_0412_mesmo_carimbo.sql
|
||||
r=$(rodar "$m" check-migration-triple.sh)
|
||||
assert_exit "$(exit_de "$r")" 1 "MIG-PROPRIA: carimbo da migration já commitada, repetido: BLOQUEIA"
|
||||
assert_contains "$(saida_de "$r")" "TIMESTAMP 20260910000000 de '20260910000000_0412_mesmo_carimbo.sql' já existe em: HEAD" "MIG-PROPRIA: e acusa o timestamp contra a própria branch"
|
||||
git -C "$m" reset -q --hard HEAD
|
||||
tripla "$m" 20260910020000_0410_de_novo.sql
|
||||
r=$(rodar "$m" check-migration-triple.sh)
|
||||
assert_exit "$(exit_de "$r")" 1 "MIG-CONTROLE: a 0410 da main encenada de novo segue bloqueada"
|
||||
git -C "$m" reset -q --hard HEAD
|
||||
tripla "$m" 20260910030000_0413_meu.sql
|
||||
r=$(rodar "$m" check-migration-triple.sh)
|
||||
assert_exit "$(exit_de "$r")" 1 "MIG-CONTROLE: a 0413 de outra branch segue bloqueada"
|
||||
assert_contains "$(saida_de "$r")" "origin/colega(20260802000000_0413_do_colega.sql)" "MIG-CONTROLE: e o dono nomeado é a branch do colega"
|
||||
git -C "$m" reset -q --hard HEAD
|
||||
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"
|
||||
|
||||
printf '\nhooks-nao-acusam-a-main: %s casos, %s falha(s)\n' "$casos" "$falhas"
|
||||
[ "$falhas" -eq 0 ] || exit 1
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
/**
|
||||
* `populacao-do-proximo-nnnn.test.ts` — o NNNN que oito lugares do repo dizem
|
||||
* estar livre, medido sobre a POPULAÇÃO da pergunta (#1273).
|
||||
*
|
||||
* A pergunta é sempre a mesma: **"algum lugar já USA ou RESERVOU este número?"**
|
||||
* — e a resposta honesta é sobre a main do PRODUTO (o remoto que aponta para
|
||||
* `melgarafael/DeskcommCRM`, com qualquer nome) mais os PRs ABERTOS, inclusive
|
||||
* de fork. Oito lugares respondiam sobre recortes menores, e a revisão mediu
|
||||
* cada um: a main de um fork em vez da do produto, só branches locais sem os
|
||||
* PRs abertos, cabeças já buscadas de PRs FECHADOS, o DISCO em vez da main
|
||||
* atual, e uma cópia VELHA da cabeça de um PR.
|
||||
*
|
||||
* A regra que diverge oito vezes é a assinatura de que ela precisa de UM lugar
|
||||
* só: `scripts/migration-populacao.sh`. Este arquivo é a prova de que esse
|
||||
* lugar mede a população certa — e, atrás de cada caso, a prova de que o lugar
|
||||
* ANTES (que o próprio arquivo nomeia) mediria diferente.
|
||||
*
|
||||
* ## O que é POPULAÇÃO e o que NÃO é
|
||||
*
|
||||
* * entra: a main do produto; `refs/heads` E `refs/remotes` juntas; e o que
|
||||
* o script de CI já soma — os PRs abertos, pela LISTA do `gh`, nunca o
|
||||
* curinga `refs/pull/*` (que persiste depois do PR fechar);
|
||||
* * não entra: a main de um fork (a `origin/main` de quem contribuiu), a
|
||||
* cópia de cabeça em `refs/remotes/<remoto>/pr/<N>` (é cópia, não PR
|
||||
* aberto), o
|
||||
* disco (a árvore de trabalho não é fonte de nada), e a cópia VELHA de uma
|
||||
* cabeça de PR quando a atual é outra.
|
||||
*
|
||||
* ## Medido, sem rede
|
||||
*
|
||||
* Cada caso monta um "principal" local e clones descartáveis, como os testes
|
||||
* de shell do repo (`tests/shell/*.test.sh`) — nada aqui toca o clone de quem
|
||||
* roda. O `gh` é neutralizado por `PATH`: o caminho dos PRs abertos é o do
|
||||
* script de CI e é exercitado aqui por CONTRATO (a lista que o `gh` falso
|
||||
* devolve), não por rede.
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
const RAIZ = path.resolve(__dirname, "../..");
|
||||
const BIBLIOTECA = path.join(RAIZ, "scripts/migration-populacao.sh");
|
||||
/** A URL que a biblioteca reconhece como o repositório PRINCIPAL. */
|
||||
const URL_PRODUTO = "https://github.com/melgarafael/DeskcommCRM.git";
|
||||
/** O repositório do produto, como o `gh` o nomeia (`--repo`). */
|
||||
const REPO_PRODUTO = "melgarafael/DeskcommCRM";
|
||||
|
||||
/**
|
||||
* Onde a biblioteca mora e quem a consome — cada consumidor é um dos oito
|
||||
* lugares da #1273, e a lista é escrita aqui para o gate que exige que TODO
|
||||
* consumidor exista. Um lugar novo que a consuma e não for listado é
|
||||
* encontrado pelo teste de "todo consumidor declara a população", abaixo.
|
||||
*/
|
||||
const CONSUMIDORES = [
|
||||
".agents/skills/deskcomm-contribuir/scripts/pre-voo.sh",
|
||||
".agents/skills/deskcomm-contribuir/scripts/hooks/check-migration-triple.sh",
|
||||
"loop/hooks/check-migration-triple.sh",
|
||||
"triagem/scripts/complemento.sh",
|
||||
] as const;
|
||||
|
||||
let TMP: string;
|
||||
|
||||
function git(dir: string, ...args: string[]): string {
|
||||
return execFileSync("git", ["-C", dir, ...args], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_CONFIG_GLOBAL: "/dev/null",
|
||||
GIT_CONFIG_SYSTEM: "/dev/null",
|
||||
GIT_AUTHOR_NAME: "Teste",
|
||||
GIT_AUTHOR_EMAIL: "teste@exemplo.invalid",
|
||||
GIT_COMMITTER_NAME: "Teste",
|
||||
GIT_COMMITTER_EMAIL: "teste@exemplo.invalid",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function escrever(dir: string, rel: string, corpo: string): void {
|
||||
const alvo = path.join(dir, rel);
|
||||
fs.mkdirSync(path.dirname(alvo), { recursive: true });
|
||||
fs.writeFileSync(alvo, corpo);
|
||||
}
|
||||
|
||||
function commitar(dir: string, msg: string): void {
|
||||
git(dir, "add", "-A");
|
||||
git(dir, "commit", "-q", "--no-verify", "-m", msg);
|
||||
}
|
||||
|
||||
/** Cria um repositório com uma migration de NNNN `nnnn` na main. */
|
||||
function repoComMigration(nnnn: string, ts = "20260901000000"): string {
|
||||
const dir = fs.mkdtempSync(path.join(TMP, "repo-"));
|
||||
git(dir, "init", "-q", "-b", "main");
|
||||
escrever(dir, `supabase/migrations/${ts}_${nnnn}_exemplo.sql`, "select 1;\n");
|
||||
commitar(dir, `migration ${nnnn}`);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone de `origem` (path local) e, com `nomeDoProduto`, um remoto `nome` que
|
||||
* aponta para a URL do REPOSITÓRIO PRINCIPAL.
|
||||
*
|
||||
* A URL é o que decide (e não o nome): o fixture tem de ter a URL do GitHub
|
||||
* escrita no config, senão a biblioteca — que faz o certo — não reconhece
|
||||
* nenhum remoto como o principal e o caso mede outra coisa. O path local
|
||||
* continua no `origin`, para servir de cópia de onde o clone foi feito.
|
||||
*/
|
||||
function clonar(origem: string, nomeDoProduto?: string): string {
|
||||
const dir = fs.mkdtempSync(path.join(TMP, "clone-"));
|
||||
execFileSync("git", ["clone", "-q", origem, dir], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_CONFIG_GLOBAL: "/dev/null",
|
||||
GIT_CONFIG_SYSTEM: "/dev/null",
|
||||
},
|
||||
});
|
||||
if (nomeDoProduto) {
|
||||
git(dir, "remote", "add", nomeDoProduto, URL_PRODUTO);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Roda um trecho bash com a biblioteca carregada, no diretório `dir`.
|
||||
* Devolve `{ stdout, rc }` — rc importa porque a ausência da main do produto
|
||||
* SAI 1, e é assim que o chamador sabe que precisa declarar.
|
||||
*/
|
||||
function bashComBiblioteca(
|
||||
dir: string,
|
||||
trecho: string,
|
||||
extraEnv: Record<string, string> = {},
|
||||
): { stdout: string; rc: number } {
|
||||
const script = `set -uo pipefail
|
||||
cd ${JSON.stringify(dir)}
|
||||
source ${JSON.stringify(BIBLIOTECA)}
|
||||
${trecho}`;
|
||||
const r = execFileSync("bash", ["-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
// NENHUM gh: a rede é o que o hook de pre-commit não pode fazer.
|
||||
PATH: "/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
...extraEnv,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
return { stdout: r, rc: 0 };
|
||||
}
|
||||
|
||||
function bashCapturandoRc(
|
||||
dir: string,
|
||||
trecho: string,
|
||||
extraEnv: Record<string, string> = {},
|
||||
): { stdout: string; rc: number } {
|
||||
const script = `set -uo pipefail
|
||||
cd ${JSON.stringify(dir)}
|
||||
source ${JSON.stringify(BIBLIOTECA)}
|
||||
${trecho}
|
||||
printf '__rc=%s\\n' "$?"`;
|
||||
const out = execFileSync("bash", ["-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: "/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
...extraEnv,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const m = out.match(/__rc=(\d+)/);
|
||||
return { stdout: out.replace(/__rc=\d+\n?$/, ""), rc: m ? Number(m[1]) : -1 };
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
TMP = fs.mkdtempSync(path.join(os.tmpdir(), "populacao-nnnn-"));
|
||||
expect(fs.existsSync(BIBLIOTECA), "scripts/migration-populacao.sh não existe").toBe(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (TMP) fs.rmSync(TMP, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("a biblioteca da população existe e é consumida por todos os oito lugares", () => {
|
||||
it("a biblioteca declara de onde tira a população e o que não cobre", () => {
|
||||
const texto = fs.readFileSync(BIBLIOTECA, "utf8");
|
||||
// Declaração de escopo: sem ela, quem lê o número não sabe o que a conta
|
||||
// cobriu (o defeito que a #1155 registrou).
|
||||
expect(texto).toContain("NÃO MEDIDO");
|
||||
expect(texto).toContain("checar:colisao-de-migration");
|
||||
// A âncora do NNNN é a do #1269, e ela é aplicada AO NOME SEM A PASTA.
|
||||
expect(texto).toContain("pop_nnnn_de");
|
||||
expect(texto).toMatch(/sed -nE 's\/\^\[0-9\]\{14\}_/);
|
||||
});
|
||||
|
||||
it.each(CONSUMIDORES)("%s carrega a biblioteca pelo caminho do repo", (c) => {
|
||||
const arquivo = path.join(RAIZ, c);
|
||||
expect(fs.existsSync(arquivo), `${c} não existe`).toBe(true);
|
||||
const texto = fs.readFileSync(arquivo, "utf8");
|
||||
expect(texto, `${c} não carrega scripts/migration-populacao.sh`).toMatch(
|
||||
/migration-populacao\.sh/,
|
||||
);
|
||||
});
|
||||
|
||||
it("o espelho .claude da skill é byte-idêntico ao .agents (gate próprio, mas a lista é uma)", () => {
|
||||
for (const rel of [
|
||||
"scripts/pre-voo.sh",
|
||||
"scripts/hooks/check-migration-triple.sh",
|
||||
]) {
|
||||
const fonte = fs.readFileSync(
|
||||
path.join(RAIZ, ".agents/skills/deskcomm-contribuir", rel),
|
||||
"utf8",
|
||||
);
|
||||
const espelho = path.join(RAIZ, ".claude/skills/deskcomm-contribuir", rel);
|
||||
if (!fs.existsSync(espelho)) continue;
|
||||
expect(fs.readFileSync(espelho, "utf8"), `espelho divergente: ${rel}`).toBe(fonte);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("a main medida é a do PRODUTO, com qualquer nome de remoto", () => {
|
||||
it("um clone cujo remoto se chama `upstream` e aponta para o principal mede a main do principal", () => {
|
||||
const principal = repoComMigration("0324");
|
||||
const clone = clonar(principal);
|
||||
// O remoto do PRODUTO pelo nome que o mantenedor usa, apontado para o
|
||||
// fixture local para a `upstream/main` EXISTIR de verdade. A URL do produto
|
||||
// vai num remoto próprio: é a URL que `pop_main_do_produto` lê, e o nome
|
||||
// `upstream` sozinho não prova nada (é o ponto do caso do insteadOf).
|
||||
git(clone, "remote", "add", "upstream", principal);
|
||||
git(
|
||||
clone,
|
||||
"config",
|
||||
"--file",
|
||||
path.join(clone, ".git/config"),
|
||||
"remote.produto.url",
|
||||
URL_PRODUTO,
|
||||
);
|
||||
|
||||
const achado = bashCapturandoRc(clone, "pop_main_do_produto").stdout.trim();
|
||||
expect(achado, "nenhum remoto foi reconhecido como o do produto").toBe("produto/main");
|
||||
|
||||
// Buscada a `upstream`, a ref EXISTE: é ela que o hook mede quando o
|
||||
// remoto do produto é o `upstream` — o caso real de quem já o configurou.
|
||||
git(clone, "fetch", "-q", "upstream");
|
||||
const r = bashCapturandoRc(clone, 'git rev-parse --verify -q "upstream/main"');
|
||||
expect(r.rc, "a main do produto não é uma ref de verdade no clone").toBe(0);
|
||||
});
|
||||
|
||||
it("FORA: a main do produto não está no clone → sai 1, e o chamador declara", () => {
|
||||
const fork = repoComMigration("0323");
|
||||
const clone = clonar(fork); // nenhum remoto é o produto: a URL local não casa
|
||||
// A URL local do fixture não é github.com, então o produto não está lá.
|
||||
const r = bashCapturandoRc(clone, "pop_main_do_produto");
|
||||
expect(r.rc, "sem o principal, a função teria de inventar uma main").toBe(1);
|
||||
expect(r.stdout.trim()).toBe("");
|
||||
|
||||
// O mesmo estado, mas a ORIGIN é o produto (URL de GitHub, sem rede) — que
|
||||
// é o caso REAL do fork: o `origin` é o principal e o `fork` é o segundo
|
||||
// remoto. Aí a main do produto é `origin/main` e o fork não é confundido.
|
||||
const comGitHub = clonar(fork);
|
||||
git(
|
||||
comGitHub,
|
||||
"config",
|
||||
"--file",
|
||||
path.join(comGitHub, ".git/config"),
|
||||
"remote.upstream.url",
|
||||
URL_PRODUTO,
|
||||
);
|
||||
const r2 = bashCapturandoRc(comGitHub, "pop_main_do_produto");
|
||||
expect(r2.stdout.trim(), "a main do produto não foi reconhecida pelo nome upstream").toBe(
|
||||
"upstream/main",
|
||||
);
|
||||
// E o fork, com URL de GitHub de outro dono, NÃO é a main do produto.
|
||||
const r3 = bashCapturandoRc(comGitHub, "pop_repo_do_produto");
|
||||
expect(r3.stdout.trim()).toBe(REPO_PRODUTO);
|
||||
});
|
||||
|
||||
it("a URL CRUA decide: o insteadOf do git não troca o principal por um fork", () => {
|
||||
const fork = repoComMigration("0323");
|
||||
const clone = clonar(fork);
|
||||
git(clone, "remote", "add", "upstream", fork);
|
||||
// O `insteadOf` faz `git remote get-url fork` devolver a URL DO PRODUTO,
|
||||
// que é o PRODUCTO de quem usasse esse comando: quem mede a main por ele
|
||||
// mede a errada. A biblioteca lê `git config` CRU e não cai nisso.
|
||||
git(
|
||||
clone,
|
||||
"config",
|
||||
"--file",
|
||||
path.join(clone, ".git/config"),
|
||||
`url.${URL_PRODUTO}.insteadOf`,
|
||||
"https://github.com/webtecnica/DeskcommCRM.git",
|
||||
);
|
||||
git(
|
||||
clone,
|
||||
"config",
|
||||
"--file",
|
||||
path.join(clone, ".git/config"),
|
||||
"remote.fork.url",
|
||||
"https://github.com/webtecnica/DeskcommCRM.git",
|
||||
);
|
||||
// O remoto do PRODUTO existe com a URL CRUA do produto; o `fork` tem a URL
|
||||
// do webtecnica. É a distinção que o caso mede.
|
||||
git(
|
||||
clone,
|
||||
"config",
|
||||
"--file",
|
||||
path.join(clone, ".git/config"),
|
||||
"remote.produto.url",
|
||||
URL_PRODUTO,
|
||||
);
|
||||
const r = bashCapturandoRc(clone, "pop_repo_do_produto");
|
||||
expect(r.stdout.trim(), "a URL crua do fork não foi confundida com a do produto").toBe(
|
||||
REPO_PRODUTO,
|
||||
);
|
||||
// E o que o `get-url` devolveria, para deixar a premissa do caso visível:
|
||||
const getUrl = execFileSync("git", ["-C", clone, "remote", "get-url", "fork"], {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
expect(getUrl, "o insteadOf parou de valer: o caso não mede o que diz medir").toContain(
|
||||
"melgarafael/DeskcommCRM",
|
||||
);
|
||||
// O remoto do PRODUTO é reconhecido, e não se confunde com o `fork` (cuja
|
||||
// URL crua é a do webtecnica, mesmo com o insteadOf apontando para cá).
|
||||
expect(bashCapturandoRc(clone, "pop_main_do_produto").stdout.trim()).toBe("produto/main");
|
||||
});
|
||||
});
|
||||
|
||||
describe("a população inclui as branches REMOTAS, e deixa de fora as cópias de PR", () => {
|
||||
it("NNNN que só existe numa branch REMOTA entra na conta", () => {
|
||||
const principal = repoComMigration("0324");
|
||||
const clone = clonar(principal);
|
||||
// Uma branch REMOTA que existe sem cópia local nenhuma. (O 0269 do PR
|
||||
// aberto #965 vivia só na CÓPIA `refs/remotes/origin/pr/965`, e essa
|
||||
// continua fora — quem o pega é o `checar`, pela lista de abertos.)
|
||||
// Precisa de um commit próprio, senão a ref resolve para o HEAD e sai da
|
||||
// conta por ser a branch de quem está rodando.
|
||||
git(clone, "checkout", "-q", "-b", "colega");
|
||||
escrever(clone, "supabase/migrations/20260902000000_0400_do_colega.sql", "select 1;\n");
|
||||
commitar(clone, "colega");
|
||||
git(clone, "checkout", "-q", "main");
|
||||
git(clone, "update-ref", "refs/remotes/origin/colega", "refs/heads/colega");
|
||||
// A cópia de cabeça de PR que a triagem deixa: `refs/remotes/origin/pr/965`.
|
||||
git(clone, "update-ref", "refs/remotes/origin/pr/965", "refs/heads/colega");
|
||||
|
||||
// O recorte ANTIGO (`git branch`) vê zero refs remotas — a premissa do caso.
|
||||
const antigo = execFileSync("git", ["-C", clone, "branch", "--format=%(refname:short)"], {
|
||||
encoding: "utf8",
|
||||
})
|
||||
.split("\n")
|
||||
.filter((l) => l.startsWith("refs/remotes/")).length;
|
||||
expect(antigo, "a premissa do caso mudou: `git branch` passou a ver refs/remotes").toBe(0);
|
||||
|
||||
const remotas = bashComBiblioteca(
|
||||
clone,
|
||||
'pop_refs_de_outrem "" | grep -c "^refs/remotes/" || true',
|
||||
).stdout.trim();
|
||||
expect(Number(remotas), "a branch remota não entrou na população").toBeGreaterThan(0);
|
||||
// E a cópia de PR fica de fora (o outro caso, aqui embaixo, mede o porquê).
|
||||
expect(
|
||||
bashComBiblioteca(clone, 'pop_refs_de_outrem "" | grep -c "origin/pr/965" || true').stdout.trim(),
|
||||
).toBe("0");
|
||||
});
|
||||
|
||||
it("a cópia em refs/remotes/*/pr/N NÃO entra: ela sobrevive ao fechamento do PR", () => {
|
||||
// É a distinção que fecha a porta dos fundos: o script de CI mede os PRs
|
||||
// ABERTOS pela LISTA do gh, e uma cópia de cabeça em refs/remotes não é
|
||||
// "PR aberto" — no clone do mantenedor eram 965 cópias contra 43 PRs
|
||||
// abertos (medido em 19/09/2026). Medi-las devolveria número de PR morto.
|
||||
const principal = repoComMigration("0324");
|
||||
const clone = clonar(principal);
|
||||
// Commit próprio: com a ref no HEAD, a exclusão seria a de "a branch de
|
||||
// quem roda", e o caso passaria pelo motivo errado.
|
||||
git(clone, "checkout", "-q", "-b", "velho");
|
||||
escrever(clone, "supabase/migrations/20260902000000_0401_de_pr_fechado.sql", "select 1;\n");
|
||||
commitar(clone, "pr que ja fechou");
|
||||
git(clone, "checkout", "-q", "main");
|
||||
git(clone, "update-ref", "refs/remotes/origin/pr/900", "refs/heads/velho");
|
||||
// A forma SEM remoto é a que a própria triagem grava (`refs/remotes/pr/N`):
|
||||
// 1336 delas no clone do mantenedor em 27/09/2026.
|
||||
git(clone, "update-ref", "refs/remotes/pr/901", "refs/heads/velho");
|
||||
const r = bashComBiblioteca(
|
||||
clone,
|
||||
'pop_refs_de_outrem "" | grep -cE "pr/90[01]" || true',
|
||||
).stdout.trim();
|
||||
expect(r).toBe("0");
|
||||
});
|
||||
|
||||
it("a ref que resolve para o HEAD (o PR de quem roda) não vira 'quem tem o número'", () => {
|
||||
const principal = repoComMigration("0324");
|
||||
const clone = clonar(principal);
|
||||
git(clone, "checkout", "-q", "-b", "fix/minha");
|
||||
git(clone, "update-ref", "refs/heads/outra", "HEAD");
|
||||
// A armadilha da #1155: a varredura acusar o autor de colidir com a si.
|
||||
const r = bashComBiblioteca(clone, 'pop_refs_de_outrem "" || true').stdout;
|
||||
expect(r, "a própria branch entrou na população").not.toContain("refs/heads/outra");
|
||||
});
|
||||
|
||||
it("NNNN de uma branch REMOTA entra no teto que a população devolve", () => {
|
||||
const principal = repoComMigration("0324");
|
||||
const clone = clonar(principal);
|
||||
// branch remota com migration 0400 — o teto do disco é 0324.
|
||||
git(clone, "checkout", "-q", "-b", "colega");
|
||||
escrever(clone, "supabase/migrations/20260902000000_0400_do_colega.sql", "select 1;\n");
|
||||
commitar(clone, "colega traz o 0400");
|
||||
git(clone, "checkout", "-q", "main");
|
||||
git(clone, "update-ref", "refs/remotes/origin/colega", "refs/heads/colega");
|
||||
|
||||
const r = bashComBiblioteca(
|
||||
clone,
|
||||
'pop_migrations $(pop_refs_de_outrem "") | pop_teto_da_populacao',
|
||||
);
|
||||
expect(r.stdout.trim(), "o 0400 da branch remota não levantou o teto").toBe("0400");
|
||||
});
|
||||
});
|
||||
|
||||
describe("o NNNN sai do nome canônico, e não do slug nem da pasta", () => {
|
||||
it("número de 4 dígitos no SLUG não vira NNNN", () => {
|
||||
// O defeito latente do item 7: `…_0326_relatorio_2024_anual.sql` fazia a
|
||||
// extração solta dar teto 2024 e esconder uma duplicata real de 0326.
|
||||
const r = bashComBiblioteca(
|
||||
RAIZ,
|
||||
`printf '%s\\n' 20260902000000_0326_relatorio_2024_anual.sql 20260903000000_0327_corrige_0241_lembrete.sql | pop_nnnn_de | sort -n | tail -1`,
|
||||
);
|
||||
expect(r.stdout.trim(), "o teto saiu do slug, não do nome canônico").toBe("0327");
|
||||
});
|
||||
|
||||
it("a duplicata real continua visível (o `sed` guloso a perdia)", () => {
|
||||
// Os nomes EXATOS da reprodução da issue: o guloso pega o ÚLTIMO `_NNNN_`,
|
||||
// que é o do SLUG, e a duplicata real de 0326 desaparece do `uniq -d`.
|
||||
// Medido nesta máquina, com estes nomes: guloso → vazio, âncora → 0326.
|
||||
const nomes =
|
||||
"20260902000000_0326_relatorio_2024_anual.sql\n" +
|
||||
"20260905000000_0326_outro_2024.sql\n" +
|
||||
"20260903000000_0327_corrige_0241_lembrete.sql";
|
||||
const anchored = bashComBiblioteca(
|
||||
RAIZ,
|
||||
`printf '%b' ${JSON.stringify(nomes)} | pop_nnnn_de | sort | uniq -d`,
|
||||
).stdout.trim();
|
||||
const guloso = bashComBiblioteca(
|
||||
RAIZ,
|
||||
`printf '%b' ${JSON.stringify(nomes)} | sed -E 's/.*_([0-9]{4})_.*/\\1/' | sort | uniq -d`,
|
||||
).stdout.trim();
|
||||
expect(anchored, "a âncora perdeu a duplicata de 0326").toBe("0326");
|
||||
expect(guloso, "a variante gulosa devia perder a duplicata (a premissa do caso)").toBe("");
|
||||
});
|
||||
|
||||
it("o caminho COM pasta também extrai (o `sed` ancorado sozinho não extrai nada)", () => {
|
||||
// Medido na revisão: `s/^([^/]*\/)?[0-9]{14}_…/` com caminho completo dá
|
||||
// 0 linhas — e a sonda passa a dizer "nenhum número" sem avisar.
|
||||
const comPasta = bashComBiblioteca(
|
||||
RAIZ,
|
||||
`printf '%s\\n' supabase/migrations/20260902000000_0326_x.sql | pop_nnnn_de`,
|
||||
).stdout.trim();
|
||||
const semPasta = bashComBiblioteca(
|
||||
RAIZ,
|
||||
`printf '%s\\n' 20260902000000_0326_x.sql | pop_nnnn_de`,
|
||||
).stdout.trim();
|
||||
expect(semPasta).toBe("0326");
|
||||
expect(comPasta, "caminho com pasta deixou de extrair").toBe("0326");
|
||||
});
|
||||
|
||||
it("a migration de nome legado, sem NNNN, é ignorada (não vira teto)", () => {
|
||||
const r = bashComBiblioteca(
|
||||
RAIZ,
|
||||
`printf '%s\\n' 00001_initial_schema.sql 20260902000000_0326_x.sql | pop_nnnn_de | sort -n | tail -1`,
|
||||
).stdout.trim();
|
||||
expect(r).toBe("0326");
|
||||
});
|
||||
});
|
||||
|
||||
describe("o que fica de fora é declarado, com o comando que cobre", () => {
|
||||
it("pop_aviso_populacao nomeia os PRs abertos e o comando que os mede", () => {
|
||||
const r = bashComBiblioteca(RAIZ, "pop_aviso_populacao origin/main");
|
||||
expect(r.stdout).toContain("PRs ABERTOS");
|
||||
expect(r.stdout).toContain("checar:colisao-de-migration");
|
||||
});
|
||||
|
||||
it("sem a main do produto, o aviso diz isso em vez de calar", () => {
|
||||
const r = bashComBiblioteca(RAIZ, "pop_aviso_populacao");
|
||||
expect(r.stdout).toContain("NÃO MEDIDO");
|
||||
expect(r.stdout).toContain("melgarafael/DeskcommCRM");
|
||||
});
|
||||
|
||||
it("o script de CI (#1269) segue sendo quem mede os PRs abertos, e declara o limite dele", () => {
|
||||
const ci = fs.readFileSync(path.join(RAIZ, "scripts/checar-colisao-de-migration.sh"), "utf8");
|
||||
expect(ci, "o script de CI deixou de declarar a lista de PRs abertos").toContain(
|
||||
"--state open",
|
||||
);
|
||||
// A âncora é a MESMA nos dois lugares: uma regra, um lugar.
|
||||
expect(ci).toMatch(/s\/\^\[0-9\]\{14\}_/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a ausência da biblioteca degrada declarado, nunca em silêncio", () => {
|
||||
it("sem a biblioteca, o chamador que a carrega com `|| true` mantém a população antiga e DIZ", () => {
|
||||
// É o contrato do `pre-voo.sh` e do hook: sem a biblioteca, o comportamento
|
||||
// é o de antes + uma linha dizendo qual é. Um hook que encolhe o universo
|
||||
// sem dizer é o defeito que a #1273 corrige.
|
||||
const dir = fs.mkdtempSync(path.join(TMP, "sem-bib-"));
|
||||
git(dir, "init", "-q", "-b", "main");
|
||||
// `--allow-empty`: um repo sem arquivo nenhum não tem o que commitar, e o
|
||||
// caso é sobre o `source` que falha, não sobre o commit.
|
||||
git(dir, "commit", "-q", "--allow-empty", "--no-verify", "-m", "vazio");
|
||||
const script = `set -uo pipefail
|
||||
cd ${JSON.stringify(dir)}
|
||||
source /caminho/que/nao/existe/migration-populacao.sh || true
|
||||
if declare -F pop_main_do_produto >/dev/null; then echo TEM; else echo "pop_main_do_produto AUSENTE"; fi`;
|
||||
const out = execFileSync("bash", ["-c", script], { encoding: "utf8" });
|
||||
expect(out).toContain("pop_main_do_produto AUSENTE");
|
||||
});
|
||||
});
|
||||
+91
-13
@@ -452,18 +452,41 @@ decisão errada **para a faixa completa**, e a razão se mede antes de começar
|
||||
própria logo abaixo):
|
||||
|
||||
```bash
|
||||
git fetch origin --force $(for n in $(gh pr list --state open --limit 100 --json number \
|
||||
--jq '.[].number'); do printf "pull/%s/head:refs/tri/%s " "$n" "$n"; done)
|
||||
for n in $(git for-each-ref --format='%(refname:short)' refs/tri/ | sed 's#refs/tri/##'); do
|
||||
git diff --name-only origin/main...refs/tri/$n | sed "s/^/$n\t/"
|
||||
# A população é a LISTA DE ABERTOS, nunca o `for-each-ref` de refs/tri/: uma
|
||||
# `refs/tri/N` é uma CÓPIA, ela sobrevive ao fechamento do PR (medido em
|
||||
# 19/09/2026: 323 refs/tri contra 35 PRs abertos, 288 delas de PR que não está
|
||||
# aberto) e ela pode estar VELHA. `--limit 1000`, e não 100 nem o padrão 30.
|
||||
git fetch --force origin $(for n in $(gh pr list --repo melgarafael/DeskcommCRM \
|
||||
--state open --limit 1000 --json number --jq '.[].number'); do
|
||||
printf "pull/%s/head:refs/tri/%s " "$n" "$n"; done)
|
||||
for n in $(gh pr list --repo melgarafael/DeskcommCRM --state open --limit 1000 \
|
||||
--json number --jq '.[].number'); do
|
||||
# `if d=$(…)` e não `git diff | sed || echo`: num cano o status é o do `sed`,
|
||||
# e o `|| echo` nunca disparava.
|
||||
if d=$(git diff --name-only origin/main...refs/tri/$n 2>/dev/null); then
|
||||
printf '%s\n' "$d" | sed "s/^/$n\t/"
|
||||
else
|
||||
echo "$n NAO MEDIDO: a cabeça não veio no fetch — refaz o fetch desse PR e meça de novo"
|
||||
fi
|
||||
done | cut -f2 | sort | uniq -c | sort -rn | head
|
||||
```
|
||||
|
||||
O `for-each-ref --format='%(refname:short)' refs/tri/ | sed 's#refs/tri/##'` que
|
||||
estava aqui devolvia `tri/1005` (o `:short` já corta `refs/`) e o `git diff`
|
||||
seguinte saía com `fatal: ambiguous argument` — a sonda inteira devolvia vazio e
|
||||
317 linhas de erro. Se ele voltar, é com `--format='%(refname)'` e
|
||||
`sed 's#^refs/tri/##'`, e ainda assim a lista certa é a de ABERTOS
|
||||
(#1273).
|
||||
|
||||
Medido em 14/09/2026, com 74 PRs abertos: `lib/i18n/dicionario.ts` tocado por **25** PRs,
|
||||
`supabase/migrations/MANIFEST.md` por **23**, `supabase/baseline.sql` por **21**. Ali todo mundo
|
||||
acrescenta no mesmo lugar, então **cada merge quebra o próximo** — e quem paga é o contribuinte
|
||||
seguinte, que recebe num PR limpo um conflito que não é dele.
|
||||
|
||||
E a lista de PRs dessa integração é a de **ABERTOS**, nunca a de `refs/tri/`: a
|
||||
cópia local sobrevive ao fechamento do PR e pode estar velha (item 3-quinquies,
|
||||
acima; e a comparação `headRefOid` de `complemento.sh`).
|
||||
|
||||
O caminho é uma **branch de integração**, com `git merge --no-ff` de cada head. Três coisas fazem
|
||||
isso funcionar, e cada uma já falhou quando ausente:
|
||||
|
||||
@@ -706,9 +729,17 @@ confere contra o **nome do arquivo** da migration, que é a fonte da verdade:
|
||||
|
||||
```bash
|
||||
git diff --name-only <base>..HEAD -- supabase/migrations/ | grep '\.sql$' \
|
||||
| sed -E 's/.*_([0-9]{4})_.*/\1/' | sort | uniq -c | awk '$1>1{print "DUPLICADO: "$2}'
|
||||
| sed 's#.*/##' | sed -nE 's#^[0-9]{14}_([0-9]{4})_.*#\1#p' \
|
||||
| sort | uniq -c | awk '$1>1{print "DUPLICADO: "$2}'
|
||||
```
|
||||
|
||||
O `sed -E 's/.*_([0-9]{4})_.*/\1/'` que estava aqui é guloso nos DOIS sentidos:
|
||||
pega o **último** `_NNNN_` do nome (o slug, quando ele tem 4 dígitos) e, com nome
|
||||
duplicado, devolve o slug dos dois em vez do NNNN — a duplicata de 0326
|
||||
desaparecia do `uniq -d` (medido: `sed` guloso vazio, âncora `0326`). A âncora do
|
||||
nome canônico é a mesma de `scripts/migration-populacao.sh` (`pop_nnnn_de`) e
|
||||
do `scripts/checar-colisao-de-migration.sh` (#1273).
|
||||
|
||||
**São TRÊS artefatos que acompanham o nome do arquivo, não dois.** O MANIFEST e o rótulo do
|
||||
apêndice no `baseline.sql` estão nos lugares onde se procura. O terceiro não: **teste que cita o
|
||||
caminho da migration**. Ele guarda o conteúdo do arquivo lendo-o do disco, e o vermelho chega como
|
||||
@@ -724,7 +755,7 @@ E confira as duas dimensões depois, porque `NNNN` único não garante timestamp
|
||||
|
||||
```bash
|
||||
ls supabase/migrations/*.sql | sed -E 's#.*/([0-9]+)_.*#\1#' | sort | uniq -d # timestamps
|
||||
ls supabase/migrations/*.sql | sed -E 's/.*_([0-9]{4})_.*/\1/' | sort | uniq -d # NNNN
|
||||
ls supabase/migrations/*.sql | sed 's#.*/##' | sed -nE 's#^[0-9]{14}_([0-9]{4})_.*#\1#p' | sort | uniq -d # NNNN (sem a pasta, senão a âncora nunca casa; com a âncora do nome canônico; o `s/.*_…_.*/` guloso pegava o slug e perdia a duplicata — #1273)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1102,10 +1133,24 @@ Dois hooks locais desta casa disparam em merge da `main` sem que você tenha edi
|
||||
| invariantes congelados | o merge traz a versão da `main` de um arquivo de `tests/invariants/` | `git diff --quiet origin/main -- <arquivo>` → **exit 0** quer dizer que ficou IDÊNTICO ao da main: foi o merge, não você |
|
||||
| tripla da migration | ele examina só arquivo com status `A`, e um `git mv` entra como `R096` — então ele **passa de graça** na renumeração | conferir à mão contra `git log --all --name-only`, os PRs abertos e as outras branches |
|
||||
|
||||
O NNNN que o hook confere é medido sobre a **população da pergunta** — a main do
|
||||
PRODUTO (o remoto que aponta para `melgarafael/DeskcommCRM`, com qualquer nome)
|
||||
mais `refs/heads` e `refs/remotes` — e não só sobre as branches locais, que era o
|
||||
recorte medido como errado na #1273. O que a população local NÃO cobre são os PRs
|
||||
abertos, e isso sai escrito na própria mensagem do hook, junto com o comando que
|
||||
cobre (`pnpm checar:colisao-de-migration`). As cópias de cabeça `refs/remotes/[<remoto>/]pr/N`
|
||||
ficam fora de propósito (sobrevivem ao fechamento do PR): o 0269 do PR aberto #965,
|
||||
que vivia só em `refs/remotes/origin/pr/965`, continua invisível ao hook — quem o
|
||||
pega é o `checar`, pela lista de abertos.
|
||||
|
||||
O escape existe (`DESKCOMM_GOV_INVARIANTS_EDIT=1`, `DESKCOMM_GOV_MIGRATION_EDIT=1`) e é legítimo
|
||||
nesses dois casos — mas **a razão vai escrita no corpo do commit, com a medição**, nunca implícita
|
||||
no uso da variável. A linha do segundo caso é a mais importante: o hook da migration **não é rede
|
||||
para renumeração**, e quem lê a mensagem dele achando que é vai renumerar contra uma régua cega.
|
||||
O que mudou na #1273 é o RECORTE, não a rede: onde media só as branches locais, o hook mede a
|
||||
main do produto mais as remotas, e a mensagem passa a **nomear** o que ficou fora (os PRs abertos)
|
||||
e a dizer onde o teto foi medido. Ele segue sem rede — hook que trava commit por `gh` troca um
|
||||
risco raro por um travamento diário.
|
||||
|
||||
### E há o caso oposto, que é pior: o merge LIMPO não chama hook nenhum
|
||||
|
||||
@@ -1123,7 +1168,7 @@ o lote 2. Nenhuma guarda viu. Quem viu foi a sonda abaixo, rodada depois de mont
|
||||
**Num trem, a colisão de migration se mede na ÁRVORE montada, nunca se confia no hook:**
|
||||
|
||||
```bash
|
||||
ls supabase/migrations/*.sql | sed -E 's/.*_([0-9]{4})_.*/\1/' | sort | uniq -d # NNNN
|
||||
ls supabase/migrations/*.sql | sed 's#.*/##' | sed -nE 's#^[0-9]{14}_([0-9]{4})_.*#\1#p' | sort | uniq -d # NNNN (sem a pasta + âncora do nome canônico — #1273)
|
||||
ls supabase/migrations/*.sql | sed -E 's#.*/([0-9]+)_.*#\1#' | sort | uniq -d # timestamp
|
||||
```
|
||||
|
||||
@@ -2332,14 +2377,47 @@ Cada um destes foi cometido de verdade nesta casa, e é por isso que estão escr
|
||||
os seguintes sem tê-los mergeado.
|
||||
|
||||
```bash
|
||||
# o que a main já tem
|
||||
git ls-tree --name-only origin/main supabase/migrations/ | grep -oE "_0[0-9]{3}_" | sort -u | tail -1
|
||||
# o que os PRs ABERTOS já reservaram
|
||||
for P in $(gh pr list --state open --json number --jq '.[].number'); do
|
||||
gh pr view $P --json files --jq '.files[].path' | grep -oE "_0[0-9]{3}_" | sed "s|^|#$P |"
|
||||
done | sort -u
|
||||
# A população da pergunta, inteira: main do produto + TODO PR aberto (inclusive
|
||||
# de fork). O script declara o que mediu e o que não mediu, e sai 1 em colisão.
|
||||
pnpm checar:colisao-de-migration
|
||||
```
|
||||
|
||||
Se a leitura for manual, três coisas são obrigatórias, e cada uma corrige um
|
||||
recorte medido que mentia: a lista de PRs precisa de `--limit 1000` (o padrão
|
||||
do gh é **30** e ele corta calado — em 19/09/2026 eram 30 de 35, e a receita
|
||||
via 12 dos 20 PRs com migration); a lista de arquivos tem de vir do git sobre a
|
||||
cabeça buscada (`git diff --name-only --diff-filter=A <base>...refs/tri/N -- supabase/migrations`),
|
||||
e não de `gh pr view --json files`, que corta em **100** arquivos por PR (o
|
||||
#1180 tinha 223, e 8 migrations novas ficavam fora dos 100 primeiros); e o NNNN
|
||||
sai com a âncora do nome canônico, `^[0-9]{14}_([0-9]{4})_`, aplicada ao NOME
|
||||
sem a pasta — `_0[0-9]{3}_` sem âncora pega o número do slug. A variável de
|
||||
repositório não é `origin/main`: é o remoto que aponta para
|
||||
`melgarafael/DeskcommCRM`, com qualquer nome (#1273).
|
||||
|
||||
**No minuto de ALOCAR, o `checar` não basta:** ele mede o arquivo que o PR JÁ
|
||||
acrescentou e, sem migration nova, responde `OK — nenhuma migration acrescentada`
|
||||
sem número nenhum. O teto é a main **mais tudo em voo, nos dois universos (NNNN e
|
||||
timestamp)**, e o que está em voo se enumera assim (cabeças ATUAIS dos abertos;
|
||||
uma linha `NNNN timestamp #PR`, e `NAO MEDIDO #N` quando a cabeça não veio):
|
||||
|
||||
```bash
|
||||
abertos=$(mktemp)
|
||||
gh pr list --repo melgarafael/DeskcommCRM --state open --limit 1000 --json number --jq '.[].number' > "$abertos"
|
||||
git fetch -q --force origin $(sed 's#.*#pull/&/head:refs/tri/&#' "$abertos")
|
||||
while read -r n; do
|
||||
if a=$(git diff --name-only --diff-filter=A origin/main...refs/tri/$n -- supabase/migrations 2>/dev/null); then
|
||||
printf '%s\n' "$a" | sed 's#.*/##' | sed -nE "s|^([0-9]{14})_([0-9]{4})_.*|\2 \1 #$n|p"
|
||||
else
|
||||
echo "NAO MEDIDO #$n (a cabeça não veio no fetch)"
|
||||
fi
|
||||
done < "$abertos" | sort
|
||||
rm -f "$abertos"
|
||||
```
|
||||
|
||||
Maior NNNN: `sort -n | tail -1` sobre essa saída e sobre a da main; maior timestamp:
|
||||
`sort -k2`. O próximo é o MAIOR + 1 nos dois (medido em 27/09/2026 com 16 abertos:
|
||||
roda igual em zsh e bash).
|
||||
|
||||
**E renumerar é três arquivos, não um.** O nome do `.sql`, o rótulo `-- ---- … (migration NNNN) ----`
|
||||
no apêndice do `baseline.sql`, e a linha do `MANIFEST.md`. No MANIFEST o número vem **colado ao slug**
|
||||
(`0208_juntar_contatos_duplicados`), então um `sed` por palavra isolada não o alcança — e o
|
||||
|
||||
@@ -4,11 +4,28 @@
|
||||
# Saída: linhas "CHAVE<TAB>VALOR". Toda linha é medição, nenhuma é veredito.
|
||||
set -uo pipefail
|
||||
N="$1"
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
COMPL_TOP="$(git rev-parse --show-toplevel)"
|
||||
cd "$COMPL_TOP"
|
||||
|
||||
DIFF=$(gh pr diff "$N" 2>/dev/null)
|
||||
FILES=$(gh pr view "$N" --json files --jq '.files[].path' 2>/dev/null)
|
||||
SHA=$(git rev-parse "refs/tri/$N" 2>/dev/null || gh pr view "$N" --json headRefOid --jq .headRefOid 2>/dev/null)
|
||||
|
||||
# A cabeça do PR é a do `gh` (headRefOid), e a cópia local `refs/tri/N` só entra
|
||||
# quando é a MESMA. A versão anterior preferia a cópia e só caía no `gh` quando
|
||||
# ela não existia — e `git rev-parse` sem `--verify` escreve o próprio argumento
|
||||
# na saída antes de falhar, então o `SHA` saía com duas linhas e a prévia do
|
||||
# merge respondia "NAO MEDIDO" mesmo com o `gh` tendo devolvido a cabeça (medido
|
||||
# no #1180). Pior: uma `refs/tri/N` VELHA media a cabeça antiga sem avisar — em
|
||||
# 19/09/2026 eram quatro PRs abertos assim (#1268, #1265, #1162, #983).
|
||||
# `--verify` cala o eco do argumento, e a comparação é o que separa as duas.
|
||||
# (issue #1273)
|
||||
SHA_GH=$(gh pr view "$N" --json headRefOid --jq .headRefOid 2>/dev/null | head -1)
|
||||
SHA_TRI=$(git rev-parse -q --verify "refs/tri/${N}^{commit}" 2>/dev/null || true)
|
||||
SHA="$SHA_GH" # a cabeça ATUAL vence sempre uma refs/tri/N divergente
|
||||
if [ -z "$SHA" ] && [ -n "$SHA_TRI" ]; then
|
||||
SHA="$SHA_TRI"
|
||||
echo "sha_velho refs/tri/${N}=${SHA_TRI:0:9} (o gh NÃO devolveu a cabeça atual; meça contra esta cópia, que pode estar atrasada)"
|
||||
fi
|
||||
|
||||
p(){ printf '%s\t%s\n' "$1" "$2"; }
|
||||
|
||||
@@ -31,6 +48,11 @@ p pr "$N"
|
||||
p sha "$SHA"
|
||||
|
||||
# --- prévia do merge (passe 3): gates rodam no merge, não na branch ---
|
||||
# E é aqui que a cópia VELHA se denuncia: com a `refs/tri/N` desatualizada, a
|
||||
# prévia media o commit antigo calado. A linha abaixo diz qual das duas cabeças
|
||||
# foi usada, e por quê (#1273).
|
||||
[ -n "$SHA_TRI" ] && [ -n "$SHA_GH" ] && [ "$SHA_TRI" != "$SHA_GH" ] && \
|
||||
echo "sha_velho refs/tri/${N}=${SHA_TRI:0:9} gh=${SHA_GH:0:9} — a prévia abaixo mede a CABEÇA ATUAL (a do gh), não a cópia local"
|
||||
if ! git cat-file -e "${SHA}^{commit}" 2>/dev/null; then
|
||||
p previa_merge "NAO MEDIDO — objeto $SHA ausente no clone (rode: git fetch origin pull/$N/head)"
|
||||
else
|
||||
@@ -48,10 +70,44 @@ if [ "$MIG" -gt 0 ]; then
|
||||
BL=$(f -c '^supabase/baseline.sql$')
|
||||
MF=$(f -c '^supabase/migrations/MANIFEST.md$')
|
||||
p migration_tripla "migrations=$MIG baseline=$BL manifest=$MF $([ "$BL" -ge 1 ] && [ "$MF" -ge 1 ] && echo COMPLETA || echo INCOMPLETA)"
|
||||
# A população é a da pergunta (#1273): a main do PRODUTO mais as outras refs do
|
||||
# clone, com a âncora do nome canônico. A versão anterior media `git ls-tree
|
||||
# origin/main` e contava com `grep -c "_${NUM}_"` — a main de um fork em vez da
|
||||
# do produto, e o número do SLUG contando como coluna (um `_0269_` no slug
|
||||
# contava como colisão que não existe). A árvore de trabalho (o PR) entra
|
||||
# também: o arquivo que o PR acrescenta É o que ocupa o número.
|
||||
if [ -r "$COMPL_TOP/scripts/migration-populacao.sh" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$COMPL_TOP/scripts/migration-populacao.sh" || true
|
||||
fi
|
||||
if declare -F pop_migrations >/dev/null 2>&1; then
|
||||
COMPL_BASE=""
|
||||
COMPL_BASE="$(pop_main_do_produto 2>/dev/null || true)"
|
||||
if [ -n "$COMPL_BASE" ] && ! git rev-parse -q --verify "${COMPL_BASE}^{commit}" >/dev/null 2>&1; then
|
||||
COMPL_BASE=""
|
||||
fi
|
||||
if [ -z "$COMPL_BASE" ]; then
|
||||
COMPL_BASE="origin/main"
|
||||
git rev-parse -q --verify "${COMPL_BASE}^{commit}" >/dev/null 2>&1 || COMPL_BASE=""
|
||||
fi
|
||||
COMPL_REFS="$(pop_refs_de_outrem "$COMPL_BASE" 2>/dev/null || true)"
|
||||
[ -n "${COMPL_REFS// /}" ] || COMPL_REFS="HEAD"
|
||||
COMPL_POP="$(pop_migrations $COMPL_REFS 2>/dev/null || true)"
|
||||
else
|
||||
COMPL_BASE="origin/main"; COMPL_POP=""
|
||||
fi
|
||||
for m in $(f '^supabase/migrations/[0-9].*\.sql$'); do
|
||||
NUM=$(basename "$m" | sed -E 's/^[0-9]+_([0-9]{4})_.*/\1/')
|
||||
COLIDE=$(git ls-tree --name-only origin/main supabase/migrations/ | grep -c "_${NUM}_")
|
||||
p migration_num "$NUM arquivo=$(basename "$m") colide_na_main=$COLIDE"
|
||||
NOME=$(basename "$m")
|
||||
NUM=$(pop_nnnn_de <<<"$NOME" 2>/dev/null || true)
|
||||
[ -n "$NUM" ] || NUM=$(sed -nE 's/^[0-9]{14}_([0-9]{4})_.+\.sql$/\1/p' <<<"$NOME")
|
||||
# O próprio arquivo do PR não é colisão dele: a lista de `pop_migrations` é a
|
||||
# das outras refs, e a linha deste arquivo é o que o PR acrescenta.
|
||||
COLIDE=NAO_MEDIDO
|
||||
if [ -n "$COMPL_POP" ]; then
|
||||
COLIDE=$(grep -E " [0-9]{14}_${NUM}_.+\.sql$" <<<"$COMPL_POP" | grep -vE " ${NOME}\$" | awk '{print $1}' | sort -u | paste -sd, - || true)
|
||||
[ -n "$COLIDE" ] || COLIDE=0
|
||||
fi
|
||||
p migration_num "$NUM arquivo=$NOME colide_em=${COLIDE} (base='${COMPL_BASE:-nenhuma}'; PRs abertos fora daqui — use pnpm checar:colisao-de-migration)"
|
||||
done
|
||||
# constraint sem dedup prévio
|
||||
c -qiE '(add constraint|unique \(|check \()' && p migration_constraint "SIM — confira dedup ANTES da constraint"
|
||||
|
||||
Reference in New Issue
Block a user