mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
802 lines
47 KiB
Bash
Executable File
802 lines
47 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Atualiza o DeskcommCRM na VPS: código novo + banco + app — com BACKUP antes e
|
||
# CHECAGEM DE SAÚDE depois. Um comando só, pensado pra quem não é técnico:
|
||
#
|
||
# bash hostgator-setup-kit/update.sh
|
||
#
|
||
# Flags:
|
||
# --force instala a versão pedida mesmo que ela seja igual ou ANTERIOR
|
||
# à que já está aqui (é o jeito explícito de voltar no tempo)
|
||
# --skip-backup pula o backup automático (não recomendado)
|
||
# --to <tag> instala essa tag em vez da mais recente publicada
|
||
# Absoluto e resolvido ANTES do `enter_project`, que faz `cd`: depois dele um
|
||
# `dirname "$0"` relativo apontaria para o lugar errado, e o único sintoma seria
|
||
# um script do kit "não encontrado" no meio da atualização.
|
||
KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
||
source "$KIT_DIR/_common.sh"
|
||
# O aviso que assume a porta enquanto o CRM esta parado. Fica em arquivo
|
||
# proprio porque so a ATUALIZACAO para o CRM — install.sh e agent.sh sourceiam
|
||
# `_common.sh` e nao tem o que anunciar.
|
||
# shellcheck source=manutencao.sh
|
||
source "$KIT_DIR/manutencao.sh"
|
||
enter_project
|
||
|
||
FORCE=""; SKIP_BACKUP=""; TARGET_TAG=""
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--force) FORCE=1 ;;
|
||
--skip-backup) SKIP_BACKUP=1 ;;
|
||
--to) shift; TARGET_TAG="$1" ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
# ── 0-. Esta cópia do repo é a dona dos contêineres? ─────────────────────────
|
||
# Antes do cron e antes do git: uma segunda cópia que atualiza por cima recria o
|
||
# parque com o .env DELA. Foi o que deixou o WhatsApp de uma VPS real três dias
|
||
# em 401. Ver `recusar_projeto_de_outra_arvore` em _common.sh.
|
||
recusar_projeto_de_outra_arvore || die "Atualização interrompida para não quebrar a instalação que está no ar."
|
||
|
||
# Single-server: o Supabase desta VPS também tem dono. E o e-mail de acesso
|
||
# (GoTrue) acompanha o SMTP do CRM AQUI, antes da decisão de versão: é este
|
||
# comando que o instalador ensina a rodar depois de configurar /admin/email, e
|
||
# "já está na versão mais recente" sairia sem entregar a troca.
|
||
if [ "${SINGLE_SERVER:-0}" = "1" ]; then
|
||
recusar_supabase_de_outra_arvore || die "Atualização interrompida para não mexer no Supabase de outra instalação."
|
||
# A porta direta do GoTrue acompanha o `signup_mode` da instalação (#1653).
|
||
# Antes do SMTP de propósito: é o caminho que roda MESMO quando o update não
|
||
# tem nada a atualizar (a saída "você já está na versão mais recente" fica
|
||
# mais abaixo), então quem trocou "só convite" na tela e rodou o update leva
|
||
# o `DISABLE_SIGNUP` no mesmo comando — e é ele que fecha
|
||
# `POST /auth/v1/signup` para quem tem a anon key. Esta chamada roda com o
|
||
# kit ANTERIOR ao checkout; a da versão nova fica dentro de
|
||
# `atualizar_supabase_single_server` (_common.sh), mais abaixo.
|
||
if sincronizar_signup_mode_do_gotrue; then
|
||
dc_supabase up -d --no-deps auth >/dev/null 2>&1 || c_ylw "⚠ Não consegui reiniciar o auth do Supabase com o modo de cadastro (#1653)."
|
||
fi
|
||
if sincronizar_smtp_do_gotrue; then
|
||
dc_supabase up -d --no-deps auth >/dev/null 2>&1 || c_ylw "⚠ Não consegui reiniciar o auth do Supabase com o SMTP do CRM."
|
||
else
|
||
c_ylw "⚠ Sem SMTP no CRM: 'esqueci a senha' e a confirmação de cadastro não enviam e-mail. Configure em /admin/email e rode o update.sh de novo."
|
||
fi
|
||
fi
|
||
|
||
# ── 0. Liga o agente da tela ANTES de qualquer decisão de versão ─────────────
|
||
# Instalar o cron aqui, e não no fim, é o que faz o bootstrap ter fim: os
|
||
# caminhos "já está na versão mais recente" e "essa versão é anterior à sua"
|
||
# saem do script mais abaixo, e se o cron dependesse deles a atualização pela
|
||
# tela nunca ligaria justamente em quem já está em dia. É idempotente.
|
||
setup_update_agent_cron
|
||
|
||
# ── 1. Tem atualização mesmo? ────────────────────────────────────────────────
|
||
step "Procurando atualizações"
|
||
git fetch --tags --quiet origin 2>/dev/null || c_ylw "⚠ não consegui falar com o GitHub — sigo com o código que já está aqui."
|
||
# A AUTORIDADE é a release publicada, NUNCA a maior tag — ver
|
||
# `ultima_release_estavel` em _common.sh. Um `TARGET_TAG` passado à mão
|
||
# continua valendo (instalar uma versão específica é operação legítima de
|
||
# quem sabe o que está fazendo); o que deixou de existir é ESCOLHER sozinho a
|
||
# maior tag, que instalaria código sem release publicada.
|
||
[ -n "$TARGET_TAG" ] || TARGET_TAG="$(ultima_release_estavel)"
|
||
[ -n "$TARGET_TAG" ] || die "Não encontrei nenhuma versão publicada para instalar. (Tag existir não basta: o alvo é a última release estável publicada no GitHub. Se o servidor não conseguiu falar com a API, tente de novo mais tarde; para instalar uma versão específica, passe --to vX.Y.Z.)"
|
||
git rev-parse --verify --quiet "${TARGET_TAG}^{commit}" >/dev/null \
|
||
|| die "Não conheço a versão $TARGET_TAG aqui. Confira o nome (ex.: v1.1.0) ou tente de novo quando o servidor conseguir falar com o GitHub."
|
||
CURRENT_TAG="$(git describe --tags --exact-match HEAD 2>/dev/null || true)"
|
||
|
||
# O código estar em dia NÃO significa que o app está: quem roda é a imagem.
|
||
# Uma atualização interrompida depois do checkout (queda de rede, falta de
|
||
# memória no meio do docker pull) deixa o repositório novo e a imagem velha — e
|
||
# a partir dali TODO update.sh respondia "já está na versão mais recente",
|
||
# prendendo o CRM na versão antiga sem nenhuma saída visível para o dono.
|
||
# Também cobre imagem republicada sem commit novo (rebuild de segurança).
|
||
# (Veio da `main`; a versão por tag cai exatamente na mesma armadilha, porque a
|
||
# comparação de tags também fica satisfeita com a imagem velha no lugar.)
|
||
image_desatualizada() {
|
||
# O fallback vem de `IMG_APP` (_common.sh, sourceado no topo deste arquivo) e não de
|
||
# um literal: num fork com namespace próprio, o literal apontava para a
|
||
# imagem do UPSTREAM, e um `.env` sem APP_IMAGE comparava o digest local
|
||
# contra um registry que não é o dele.
|
||
local img="${APP_IMAGE:-${IMG_APP}:latest}" local_d remote_d
|
||
local_d="$(docker image inspect "$img" --format '{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}' 2>/dev/null | sed 's/.*@//')"
|
||
[ -z "$local_d" ] && return 0 # nem baixada ainda → atualizar
|
||
remote_d="$(docker buildx imagetools inspect "$img" 2>/dev/null | awk '/^Digest:/{print $2; exit}')"
|
||
[ -z "$remote_d" ] && return 1 # sem como consultar → não forçar
|
||
[ "$local_d" != "$remote_d" ]
|
||
}
|
||
|
||
MESMA_TAG=""
|
||
[ "$CURRENT_TAG" = "$TARGET_TAG" ] && MESMA_TAG=1
|
||
|
||
if [ -n "$MESMA_TAG" ] && [ -z "$FORCE" ] && ! image_desatualizada; then
|
||
# ⛔ O AVISO TAMBÉM DESCE AQUI — esta saída é anterior ao `manutencao_desce`
|
||
# do fluxo normal (mais abaixo) e ao `restaurar_servicos` do caminho de erro.
|
||
#
|
||
# MEDIDO numa VPS real: uma atualização morreu logo depois de `manutencao_sobe`
|
||
# (tag nova já no disco, imagem antiga ainda rodando). O aviso ficou de pé com
|
||
# o apelido de rede `app`, e o Caddy passou a entregar ELE — 503 em tudo:
|
||
# site, crons e webhook do WAHA. O app estava saudável o tempo todo.
|
||
#
|
||
# A volta por cima não existia: como o `git checkout` da tag JÁ tinha
|
||
# acontecido, toda execução seguinte caía nesta linha, dizia "nada a
|
||
# atualizar" e saía — sem nunca tocar no aviso. O CRM ficou 6h30 fora do ar
|
||
# e nem o botão da tela voltava, porque o agente do host também levava 503.
|
||
#
|
||
# `manutencao_desce` é `docker rm -f ... || true`: idempotente, custa nada
|
||
# quando não há aviso nenhum de pé, que é o caso comum desta saída.
|
||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$NOME_DA_MANUTENCAO"; then
|
||
manutencao_desce
|
||
c_ylw "⚠ Havia um aviso de manutenção preso de uma atualização anterior — removido."
|
||
c_ylw " Enquanto ele estava de pé, o CRM respondia 503 para todo mundo."
|
||
# Aviso preso = a execução anterior morreu no meio (banco e/ou imagem pela
|
||
# metade); "nada a atualizar" sozinho deixaria o app na imagem antiga.
|
||
c_ylw " A atualização anterior não terminou. Para concluí-la:"
|
||
c_ylw " bash hostgator-setup-kit/update.sh --to $TARGET_TAG --force"
|
||
fi
|
||
c_grn "✓ Você já está na versão mais recente ($TARGET_TAG). Nada a atualizar."
|
||
exit 0
|
||
fi
|
||
|
||
# Alvo que JÁ está contido no que roda aqui = andar pra trás, não pra frente.
|
||
# Numa instalação que segue a `main`, `git describe --exact-match` é vazio: a
|
||
# comparação de tags acima passa batido e, sem esta guarda, o script instalaria
|
||
# alegremente uma versão MAIS VELHA que a instalada — desligando o que o dono
|
||
# já tem (foi assim que este próprio botão se autodestruiria, voltando pra uma
|
||
# imagem que não conhece o agente de atualização). Recusar é o padrão; voltar
|
||
# no tempo continua possível, mas só quando alguém pede de propósito.
|
||
# Quando o alvo é a MESMA tag já instalada, a guarda não se aplica: não há para
|
||
# onde voltar no tempo — só a imagem é que ficou para trás.
|
||
if [ -z "$FORCE" ] && [ -z "$MESMA_TAG" ]; then
|
||
is_already_in_head "$TARGET_TAG" && CONTIDA=0 || CONTIDA=$?
|
||
case "$CONTIDA" in
|
||
0) refuse "A versão $TARGET_TAG é ANTERIOR à que já está instalada neste servidor.
|
||
Instalar ela seria voltar no tempo e desligar coisas que você já tem.
|
||
Não mexi em nada: nem no banco, nem no app — está tudo como estava.
|
||
Se você REALMENTE quer voltar para a $TARGET_TAG, rode:
|
||
bash hostgator-setup-kit/update.sh --to $TARGET_TAG --force" ;;
|
||
2) refuse "Não consegui ter CERTEZA de que a versão $TARGET_TAG é mais nova que a instalada
|
||
aqui — a cópia do código neste servidor veio abreviada e eu não consegui completá-la
|
||
(o servidor precisa conseguir falar com o GitHub para isso).
|
||
Prefiro não mexer a arriscar te levar para uma versão anterior sem querer.
|
||
Não mexi em nada. Tente de novo em alguns minutos; se insistir, confira a internet do
|
||
servidor. Para instalar assim mesmo, por sua conta:
|
||
bash hostgator-setup-kit/update.sh --to $TARGET_TAG --force" ;;
|
||
esac
|
||
fi
|
||
if [ -n "$MESMA_TAG" ] && [ -n "$FORCE" ]; then
|
||
# Com --force na mesma tag ninguém conferiu a imagem: quem chega aqui pediu
|
||
# para refazer (é a saída que a própria atualização ensina quando o banco não
|
||
# termina limpo). Dizer "o app está rodando uma imagem antiga" seria inventar.
|
||
if [ -n "$SKIP_BACKUP" ]; then
|
||
c_ylw "Refazendo a versão $TARGET_TAG, como pedido (--force): banco de novo, e confere o app."
|
||
else
|
||
c_ylw "Refazendo a versão $TARGET_TAG, como pedido (--force): backup e banco de novo, e confere o app."
|
||
fi
|
||
elif [ -n "$MESMA_TAG" ]; then
|
||
c_ylw "O código já está na $TARGET_TAG, mas o app está rodando uma imagem antiga. Vou atualizar a imagem."
|
||
else
|
||
c_ylw "Vou atualizar para a versão $TARGET_TAG com segurança."
|
||
fi
|
||
|
||
# ── 2. Backup de segurança ANTES de tocar no banco ───────────────────────────
|
||
if [ -z "$SKIP_BACKUP" ]; then
|
||
step "Backup de segurança (antes de mexer no banco)"
|
||
if bash "$KIT_DIR/backup.sh"; then
|
||
c_grn "✓ backup feito — se algo der errado, dá pra restaurar (restore.sh)."
|
||
else
|
||
if [ -n "${DESKCOMM_AGENT_REPORT:-}" ] || [ ! -t 0 ]; then
|
||
die "O backup preventivo falhou. Atualização automática interrompida para proteger os dados."
|
||
fi
|
||
c_ylw "⚠ o backup falhou. A atualização NÃO apaga dados (só reorganiza os contatos),"
|
||
c_ylw " mas o ideal é ter backup."
|
||
read -r -p "Deseja continuar MESMO SEM BACKUP? Digite 'CONTINUAR': " conf
|
||
[ "$conf" = "CONTINUAR" ] || die "Atualização cancelada pelo operador para investigar a falha do backup."
|
||
fi
|
||
fi
|
||
# Avisa o agente do host (se for ele quem está dirigindo) — é o que faz a tela
|
||
# de atualização avançar passo a passo enquanto o app ainda está de pé.
|
||
[ -n "${DESKCOMM_AGENT_REPORT:-}" ] && eval "${DESKCOMM_AGENT_REPORT_CMD}" backup
|
||
|
||
# ── 3. Código novo ───────────────────────────────────────────────────────────
|
||
step "Baixando o código novo"
|
||
if ! git checkout --quiet "$TARGET_TAG" 2>&1; then
|
||
die "Não consegui trocar para a versão $TARGET_TAG (parece haver mudanças locais que divergem).
|
||
Rode 'git status' pra ver, ou peça ajuda. NÃO mexi no banco — está tudo como estava."
|
||
fi
|
||
|
||
# As funções do kit são carregadas na linha 16, ANTES deste checkout — então,
|
||
# sem esta releitura, o resto desta atualização roda com as funções da versão
|
||
# ANTIGA, e todo conserto que viva numa função do kit só chega na atualização
|
||
# SEGUINTE. Foi medido numa VPS de produção em 17/09/2026: depois de atualizar
|
||
# para a versão que conserta a linha do cron (que deixava um segredo escrito no
|
||
# crontab, e portanto no syslog), a linha antiga continuava lá — o conserto
|
||
# existia no disco e não tinha rodado. Duas passadas para aplicar um conserto é
|
||
# o mesmo que exigir passo manual de quem opera a VPS, e a doutrina de
|
||
# packaging proíbe.
|
||
#
|
||
# `_common.sh` só define funções e constantes no topo (`set -euo pipefail`,
|
||
# COMPOSE, cores, REFUSED_RC), então reler é idempotente: nada é reexecutado
|
||
# com efeito. O que muda é de onde vêm as funções daqui para baixo.
|
||
source "$KIT_DIR/_common.sh"
|
||
# E o aviso de manutenção pelo MESMO motivo, na mesma linha do raciocínio acima:
|
||
# ele também é carregado no topo, também é só definição de função, e o passo que
|
||
# o USA (a pausa do banco) vem depois daqui. Sem esta linha o parágrafo acima
|
||
# valeria para `_common.sh` e seria falso para o kit — um conserto na página de
|
||
# manutenção chegaria uma atualização atrasada, que é exatamente o defeito que a
|
||
# releitura existe para fechar.
|
||
source "$KIT_DIR/manutencao.sh"
|
||
|
||
# Single-server: o Supabase vai para a versão pinada no código novo ANTES do
|
||
# banco (o passo 4 pausa peças dele, e um `up` depois as religaria).
|
||
if [ "${SINGLE_SERVER:-0}" = "1" ]; then
|
||
# Esta função também sincroniza o modo de cadastro com o GoTrue (#1653). A
|
||
# chamada mora DENTRO dela, e não numa linha aqui, porque é o corpo dela que
|
||
# o update.sh antigo executa na atualização que traz o conserto.
|
||
atualizar_supabase_single_server || die "O Supabase desta VPS não subiu (erro acima). NÃO mexi no banco do CRM."
|
||
fi
|
||
|
||
[ -n "${DESKCOMM_AGENT_REPORT:-}" ] && eval "${DESKCOMM_AGENT_REPORT_CMD}" codigo
|
||
|
||
# ── 4. Banco: schema + correções de dados (schema ANTES do app) ──────────────
|
||
# O baseline é idempotente e auto-curativo. Re-aplicar numa base que JÁ existe
|
||
# gera erros do tipo "já existe" / "multiple primary keys" — isso é ESPERADO e
|
||
# inofensivo (são objetos que já estavam lá). Filtramos esse ruído e só
|
||
# mostramos problemas de verdade. Erro de disputa com o app no ar (deadlock)
|
||
# faz o arquivo ser aplicado de novo: ver `reaplicar_baseline` em _common.sh.
|
||
# Re-aplicar o baseline é DDL, então vai por `url_do_schema` (_common.sh) e não
|
||
# pela string do app: numa instalação em Supabase próprio, com a role menor no
|
||
# `.env` como recomendamos, este passo passava a falhar em silêncio a cada
|
||
# atualização — e é o update.sh que entrega migration nova ao clone (issue #192).
|
||
# ── NINGUÉM FALA COM O BANCO ENQUANTO ELE MUDA ───────────────────────────────
|
||
#
|
||
# Medido nesta instalação, no mesmo dia e com o mesmo arquivo:
|
||
# tudo de pé ................................ 113 travamentos
|
||
# CRM parado ................................ 60 travamentos
|
||
# CRM + rest + realtime + studio parados .... 0 travamentos
|
||
#
|
||
# Travamento aqui não é lentidão: quando o `create policy` trava, o `drop` que
|
||
# veio antes já valeu. A regra some, o banco nega a leitura em silêncio, e a
|
||
# tela fica vazia — indistinguível de "não há nada aqui".
|
||
#
|
||
# Custa ~16s (medido: parar 10,3s, subir 6,0s) numa atualização cuja mediana
|
||
# real é 308s e cuja variação natural entre duas rodadas foi de 785s. Fica
|
||
# abaixo do ruído que já existe.
|
||
#
|
||
# O `trap` é o que impede um erro no meio de deixar a instalação pela metade:
|
||
# qualquer saída — sucesso, erro ou interrupção — devolve as peças do Supabase.
|
||
# EXIT nao basta: interrupcao (Ctrl+C, cron matando a rodada, reinicio da
|
||
# maquina) nao passa por ele em todos os casos — e o desfecho seria a
|
||
# instalacao com as pecas do banco paradas, que foi o que se mediu.
|
||
trap restaurar_servicos EXIT INT TERM HUP
|
||
|
||
step "Atualizando o banco de dados"
|
||
# O que sobrou de errado no banco, para ser repetido no FIM da execução.
|
||
# Vazio = o banco terminou limpo (ou não havia baseline para aplicar).
|
||
BANCO_INCOMPLETO=""
|
||
# As linhas que repetir NÃO cura: as que não são de disputa nem de conexão
|
||
# (permissão, dado). Vazio com BANCO_INCOMPLETO cheio = só o banco ocupado.
|
||
BANCO_RESTANTE=""
|
||
# O que fazer, dito por causa, no passo 4 e de novo no fim. Rodar o update.sh sem
|
||
# --force responderia "já está na versão mais recente" e não tocaria no banco.
|
||
# O restore vem por ÚLTIMO: ele desfaz também o que o CRM gravou desde o backup.
|
||
orientar_banco_incompleto() {
|
||
# As duas metades SOMAM: uma lista pode ter disputa (que repetir cura) e erro de
|
||
# permissão ou de dado (que não). Escolher uma só escondia a ação possível.
|
||
if [ "$BANCO_RESTANTE" != "$BANCO_INCOMPLETO" ]; then
|
||
c_ylw " Parte não aplicou porque o banco seguiu ocupado ou fora de alcance nas $BASELINE_PASSADAS passadas."
|
||
c_ylw " Confira se o banco está no ar e repita a atualização, de preferência num horário de pouco"
|
||
c_ylw " movimento (reaplica o banco; o site pode piscar por alguns segundos):"
|
||
c_ylw " bash hostgator-setup-kit/update.sh --to $TARGET_TAG --force"
|
||
fi
|
||
case "$BANCO_RESTANTE" in
|
||
"") ;;
|
||
*permission\ denied*|*must\ be\ owner*|*permissão\ negada*)
|
||
c_ylw " Há erros de PERMISSÃO, e esses repetir não cura: a conexão do .env não é o dono do banco."
|
||
c_ylw " Num Supabase próprio, declare SUPABASE_DB_ADMIN_URL no .env — é ela que roda o schema — e"
|
||
c_ylw " repita a atualização:"
|
||
c_ylw " bash hostgator-setup-kit/update.sh --to $TARGET_TAG --force" ;;
|
||
*)
|
||
c_ylw " O resto dos erros acima repetir não cura: guarde a mensagem e peça ajuda." ;;
|
||
esac
|
||
c_ylw " Só em último caso, volte ao backup feito antes desta atualização (restore.sh)."
|
||
}
|
||
if [ -f supabase/baseline.sql ]; then
|
||
# O aviso PRIMEIRO: entre pausar e anunciar, quem estivesse com a tela aberta
|
||
# veria o erro do navegador, que e o desfecho que esta onda existe para tirar.
|
||
manutencao_sobe
|
||
pausar_o_que_fala_com_o_banco
|
||
# Extensões que o schema exige (idempotente; iguais ao install.sh).
|
||
pg_container postgres:17-alpine psql "$(url_do_schema)" -c \
|
||
"create extension if not exists vector with schema public; create extension if not exists citext with schema public; create extension if not exists pg_trgm with schema public;" \
|
||
>/dev/null 2>&1 || true
|
||
|
||
# ── O LOG DO BANCO FICA GUARDADO ──────────────────────────────────────────
|
||
#
|
||
# Ele era descartado: a saída do psql servia só para o filtro de erros e
|
||
# morria com a função. O que o agente guarda em `system_update_runs.log_tail`
|
||
# é a CAUDA da atualização — Docker e reinício —, e o banco acontece antes.
|
||
#
|
||
# Medido em 2026-09-12, numa instalação real: duas regras de isolamento
|
||
# sumiram durante uma atualização, o funil ficou vazio para todo mundo, e não
|
||
# houve como saber por quê — a evidência tinha sido jogada fora. A única coisa
|
||
# que restou foi a hipótese.
|
||
#
|
||
# O segundo argumento de `reaplicar_baseline` já existe para isto e recebe
|
||
# TODAS as passadas, cada uma com cabeçalho — melhor que a saída da última.
|
||
if reaplicar_baseline "$PROJECT_DIR/supabase/baseline.sql" "$PROJECT_DIR/.deskcomm-banco.log"; then
|
||
if [ "$BASELINE_PASSADAS" -gt 1 ]; then
|
||
c_grn "✓ banco atualizado na passada $BASELINE_PASSADAS — as anteriores não aplicaram tudo (banco ocupado ou conexão instável; o que faltou está listado acima)."
|
||
else
|
||
c_grn "✓ banco atualizado (e conversas reorganizadas, se havia bagunça)."
|
||
fi
|
||
else
|
||
BANCO_INCOMPLETO="$BASELINE_INESPERADO"
|
||
BANCO_RESTANTE="$(printf '%s\n' "$BANCO_INCOMPLETO" | grep -viE "$BASELINE_ERROS_DE_DISPUTA" || true)"
|
||
c_ylw "⚠ Apareceram avisos no banco que NÃO são os esperados:"
|
||
# Sem `| head`: com pipefail, o head que fecha cedo mata o printf com SIGPIPE
|
||
# numa lista grande — e o set -e derrubava o script aqui, antes do aviso de
|
||
# PERMISSÃO, que foi escrito justamente para ela.
|
||
listar_erros_do_banco "$BANCO_INCOMPLETO" 20
|
||
c_ylw " O app pode ainda funcionar."
|
||
orientar_banco_incompleto
|
||
fi
|
||
# ── E AS REGRAS DE ISOLAMENTO SÃO CONFERIDAS ──────────────────────────────
|
||
#
|
||
# ## Por que isto existe
|
||
#
|
||
# O baseline aplica cada regra como APAGAR e depois CRIAR — é o único jeito
|
||
# portável, porque o Postgres não tem `create or replace policy`. E esta
|
||
# atualização roda SEM parar em erro, de propósito, para um clone bagunçado
|
||
# conseguir se curar.
|
||
#
|
||
# As duas coisas juntas têm um desfecho ruim: se o "criar" falha, o "apagar"
|
||
# já valeu. A regra some, a atualização segue e reporta SUCESSO. Com a regra
|
||
# de leitura ausente e a segurança por linha ligada, o Postgres nega tudo —
|
||
# sem erro, sem aviso. A tela mostra uma lista vazia, que é indistinguível de
|
||
# "não há nada aqui".
|
||
#
|
||
# Medido: o dono de uma instalação descobriu horas depois, pelo funil vazio, e
|
||
# não pela atualização que tinha acabado de dizer "concluída com sucesso".
|
||
#
|
||
# ## A régua, e por que não é "toda regra que o arquivo cria"
|
||
#
|
||
# O baseline CRIA e depois APAGA a mesma regra de propósito em vários pontos —
|
||
# é assim que uma regra antiga vira três novas (`conversations_agent_write`
|
||
# virou insert/update/delete). Contar toda criação daria falso positivo em
|
||
# cima de decisão deliberada, e falso positivo derruba a confiança no aviso
|
||
# inteiro. Vale a ÚLTIMA operação de cada regra no arquivo: quem termina
|
||
# criada é esperada; quem termina apagada, não.
|
||
#
|
||
# ── E A COMPARAÇÃO RODA EM ORDEM DE BYTES, SEMPRE ─────────────────────────
|
||
#
|
||
# ⛔ `sort` e `comm` precisam concordar na ordenação. No GNU coreutils os dois
|
||
# seguem o mesmo locale e concordam (medido: Ubuntu 20.04–25.10, Debian 12,
|
||
# AlmaLinux 8/9 dão 0 em C, C.UTF-8, en_US e pt_BR). No Ubuntu 26.04, que troca
|
||
# o coreutils pelo uutils (Rust, 0.8.0), o `sort` ordena pelo locale e o `comm`
|
||
# compara BYTES: sob en_US/pt_BR.UTF-8, `orgs_select` cai entre `org_guardrail_*`
|
||
# e `org_voice_calls_*`, o `comm` perde o passo — "comm: file 2 is not in
|
||
# sorted order" na stderr — e o que devolve depois é lixo.
|
||
#
|
||
# MEDIDO numa instalação real, 2026-09-28: com as 114 regras TODAS no banco, a
|
||
# comparação acusou 2 faltando (`org_voice_calls_admin_write` e
|
||
# `org_voice_calls_select`). O alarme falso faz o script tentar recriar as
|
||
# duas, o banco responde "already exists", a conferência seguinte tropeça no
|
||
# mesmo erro de ordenação — e a atualização PARA, deixando o aviso de
|
||
# manutenção de pé. O CRM passou 8 horas em 503 com o banco íntegro, e a tela
|
||
# mandava o dono procurar regra que nunca faltou.
|
||
#
|
||
# A cura é forçar o locale da comparação para `C`, que é ordem de bytes: aí o
|
||
# `sort` produz exatamente o que o `comm` espera, em qualquer ambiente.
|
||
#
|
||
# ⚠️ `LC_COLLATE=C` NÃO basta, e a diferença custa uma sessão de depuração:
|
||
# o POSIX dá precedência a `LC_ALL` sobre `LC_COLLATE`, então basta alguém
|
||
# exportar `LC_ALL=…UTF-8` — systemd, um `docker exec`, o terminal de quem
|
||
# roda o update à mão — para o pin virar enfeite e o defeito voltar inteiro.
|
||
# MEDIDO, com as 114 regras reais: `LC_ALL=C` devolve 0 em qualquer condição;
|
||
# `LC_COLLATE=C` devolve 0 com `LC_ALL` vazio e 2 com `LC_ALL` preenchido.
|
||
# `LC_ALL=C` vale só para os comandos abaixo — não alcança as mensagens.
|
||
esperadas="$(awk '
|
||
match($0, /drop policy if exists "?[a-zA-Z0-9_]+"? on public\.[a-zA-Z0-9_]+/) {
|
||
linha = substr($0, RSTART, RLENGTH); acao = "drop"
|
||
}
|
||
match($0, /create policy "?[a-zA-Z0-9_]+"? on public\.[a-zA-Z0-9_]+/) {
|
||
linha = substr($0, RSTART, RLENGTH); acao = "create"
|
||
}
|
||
acao != "" {
|
||
gsub(/.*policy (if exists )?"?/, "", linha); gsub(/"? on public\./, "|", linha)
|
||
estado[linha] = acao; acao = ""
|
||
}
|
||
END { for (k in estado) if (estado[k] == "create") print k }
|
||
' supabase/baseline.sql | LC_ALL=C sort -u)"
|
||
|
||
# ── E SÓ SE COBRE QUEM TEM A RELAÇÃO NO BANCO ──────────────────────────────
|
||
#
|
||
# MEDIDO na issue #1897: as 8 regras de honorários moram DENTRO do corpo de
|
||
# public.fn_honorarios_provisionar() (supabase/baseline.sql:36801, primeira
|
||
# policy em :36896), e essa função só executa quando um administrador chama
|
||
# fn_modulo_instalar('honorarios', …) — criar a função não cria tabela nenhuma,
|
||
# como a própria migration 0480 / ADR-0002 avisa. Num VPS SEM o módulo,
|
||
# honorarios_contratos e honorarios_parcelas não existem: o awk de cima enxerga
|
||
# o `create policy` no TEXTO do arquivo, a recriação responde
|
||
# `relation does not exist`, a segunda conferência acusa as MESMAS 8 e o script
|
||
# sai em 1 com o CRM parado — era a atualização inteira de toda instalação sem
|
||
# o módulo de honorários (a tela mostrava as 8 e mais nada).
|
||
#
|
||
# A régua passa a cobrir só policy cuja RELAÇÃO já existe em `public`. É mais
|
||
# genérico do que caçar `$f$`/`$$` no texto: cobre os próximos módulos da
|
||
# ADR-0002, venham eles por corpo de função, por migration ou por qualquer
|
||
# outra forma de escrever o baseline. E NÃO afrouxa nada: policy de tabela que
|
||
# EXISTE continua sendo cobrada, que é o caso para o qual o aviso existe — a
|
||
# regra que some do banco com a tabela de pé continua derrubando a atualização.
|
||
#
|
||
# ⚠️ Se a consulta vier VAZIA (banco fora do ar, URL trocada), NÃO se filtra.
|
||
# Sem a lista de relações, filtrar derrubaria `esperadas` inteira e o ✓ sairia
|
||
# com "0 declaradas" — o aviso viraria mudo exatamente quando ninguém consegue
|
||
# ler o banco. Vale o comportamento antigo, que é barulhento: tudo é cobrado e
|
||
# a conferência para. Surdo nunca.
|
||
tabelas="$(pg_container -i postgres:17-alpine psql "$(url_do_schema)" -t -A -c \
|
||
"select c.relname from pg_class c join pg_namespace n on n.oid=c.relnamespace
|
||
where n.nspname='public';" 2>/dev/null | LC_ALL=C sort -u)"
|
||
|
||
if [ -n "$tabelas" ]; then
|
||
# `esperadas` é `regra|tabela`; o filtro olha só a tabela. A saída do awk
|
||
# segue a ordem do SEGUNDO arquivo (o `esperadas` já ordenado), e o
|
||
# segundo `sort` reforça o MESMO pino de antes (LC_ALL=C): o `comm` logo
|
||
# abaixo lê em `LC_ALL=C` e não perdoa entrada fora de ordem.
|
||
esperadas="$(awk 'NR == FNR { existe[$0] = 1; next }
|
||
{ split($0, par, "[|]"); if (existe[par[2]]) print }' \
|
||
<(printf '%s\n' "$tabelas") <(printf '%s\n' "$esperadas") | LC_ALL=C sort -u)"
|
||
fi
|
||
|
||
existentes="$(pg_container -i postgres:17-alpine psql "$(url_do_schema)" -t -A -F'|' -c \
|
||
"select p.polname, c.relname from pg_policy p join pg_class c on c.oid=p.polrelid
|
||
join pg_namespace n on n.oid=c.relnamespace where n.nspname='public';" 2>/dev/null | LC_ALL=C sort -u)"
|
||
|
||
faltando="$(LC_ALL=C comm -23 <(printf '%s\n' "$esperadas") <(printf '%s\n' "$existentes") || true)"
|
||
|
||
if [ -n "$faltando" ]; then
|
||
# ── RECRIAR AS QUE FALTAM, NUNCA REAPLICAR O ARQUIVO ─────────────────────
|
||
#
|
||
# ⚠️ Isto corrige o que este script fazia antes: reaplicar o baseline inteiro
|
||
# e conferir de novo. Aquilo era o que eu tinha feito no servidor, e a
|
||
# medição mostrou que NÃO FECHA — reaplicar não converge. A segunda passada
|
||
# devolveu `conversations_select` e levou embora `conversations_agent_insert`;
|
||
# a terceira trocou o conjunto outra vez. Cada passada sorteia, porque cada
|
||
# passada é a mesma corrida de APAGAR e CRIAR 92 vezes.
|
||
#
|
||
# Recriar só o que falta é um punhado de comandos rápidos, com muito menos
|
||
# superfície para travar. E roda com os serviços ainda PARADOS, que é a
|
||
# única janela sem disputa.
|
||
#
|
||
# E não é uma segunda cópia das 92 declarações: o comando sai do PRÓPRIO
|
||
# `baseline.sql`, recortado dele. Nada aqui sabe o que uma regra diz.
|
||
c_ylw "⚠ Faltaram regras de isolamento. Recriando exatamente as que faltam…"
|
||
faltam_arq="$PROJECT_DIR/.deskcomm-regras-faltando.txt"
|
||
printf '%s\n' "$faltando" > "$faltam_arq"
|
||
|
||
# A régua junta o comando INTEIRO — uma regra real ocupa várias linhas, e
|
||
# recortar só a primeira produziria SQL sem predicado e sem `;`, que falha
|
||
# deixando a impressão de que tentou. E vale a ÚLTIMA operação de cada
|
||
# regra: quem o arquivo cria e depois apaga de propósito não é recriada.
|
||
# /!\ O arquivo do que falta entra como PRIMEIRO ARQUIVO do awk, e nao por
|
||
# `-v`. MEDIDO: `awk -v var=valor` processa sequencias de escape no valor,
|
||
# entao um caminho do Windows (C:\Users\...) perde as barras e o awk le um
|
||
# arquivo que nao existe — devolvendo vazio, EM SILENCIO, como se nada
|
||
# faltasse. Numa VPS Linux nao doeria; o teste pegou antes de virar aposta.
|
||
recria="$(awk '
|
||
NR == FNR { sub(/[ \t\r]+$/, "", $0); if ($0 != "") quero[$0] = 1; next }
|
||
/create policy|drop policy if exists/ { buf = ""; coletando = 1 }
|
||
coletando { buf = buf $0 "\n" }
|
||
coletando && /;[ \t]*$/ {
|
||
coletando = 0
|
||
if (match(buf, /drop policy if exists "?[a-zA-Z0-9_]+"? on public\.[a-zA-Z0-9_]+/)) { k = substr(buf, RSTART, RLENGTH); acao = "drop" }
|
||
else if (match(buf, /create policy "?[a-zA-Z0-9_]+"? on public\.[a-zA-Z0-9_]+/)) { k = substr(buf, RSTART, RLENGTH); acao = "create" }
|
||
else next
|
||
gsub(/.*policy (if exists )?"?/, "", k); gsub(/"? on public\./, "|", k)
|
||
estado[k] = acao; if (acao == "create") texto[k] = buf
|
||
}
|
||
END { for (k in quero) if (estado[k] == "create") printf "%s", texto[k] }
|
||
' "$faltam_arq" supabase/baseline.sql)"
|
||
|
||
if [ -n "$recria" ]; then
|
||
printf '%s\n' "$recria" | pg_container -i postgres:17-alpine \
|
||
psql "$(url_do_schema)" >> "$PROJECT_DIR/.deskcomm-banco.log" 2>&1 || true
|
||
fi
|
||
rm -f "$faltam_arq"
|
||
|
||
existentes="$(pg_container -i postgres:17-alpine psql "$(url_do_schema)" -t -A -F'|' -c \
|
||
"select p.polname, c.relname from pg_policy p join pg_class c on c.oid=p.polrelid
|
||
join pg_namespace n on n.oid=c.relnamespace where n.nspname='public';" 2>/dev/null | LC_ALL=C sort -u)"
|
||
# Mesma ordenação da primeira conferência, e pelo mesmo motivo: `LC_ALL=C`
|
||
# é o que faz `sort` e `comm` concordarem. Ver o bloco acima.
|
||
faltando="$(LC_ALL=C comm -23 <(printf '%s\n' "$esperadas") <(printf '%s\n' "$existentes") || true)"
|
||
fi
|
||
|
||
if [ -n "$faltando" ]; then
|
||
c_red "⛔ REGRAS DE ISOLAMENTO AUSENTES — NÃO use o sistema até resolver."
|
||
c_red " Sem elas o banco NEGA a leitura em silêncio: telas aparecem VAZIAS,"
|
||
c_red " sem erro nenhum, e isso é indistinguível de 'não há dados'."
|
||
printf '%s\n' "$faltando" | sed 's/|/ na tabela /; s/^/ • /' | head -20
|
||
c_ylw " O log do banco está em .deskcomm-banco.log — mande-o para o suporte."
|
||
c_ylw " Para voltar ao estado anterior: bash restore.sh"
|
||
# ⛔ E A ATUALIZAÇÃO PARA AQUI.
|
||
#
|
||
# Antes ela seguia: imprimia este bloco vermelho e ia para o passo 5, que
|
||
# sobe o app com a imagem nova. O CRM voltava ao ar sem regra de isolamento,
|
||
# mostrando tela vazia para todo mundo — e o vermelho já tinha rolado para
|
||
# fora da tela. Foi assim que o dono da instalação descobriu pelo funil,
|
||
# horas depois, e não pela atualização.
|
||
#
|
||
# O `trap` (logo acima do passo do banco) devolve as peças do Supabase e
|
||
# deixa o CRM parado de propósito. Um CRM fora do ar é um problema visível
|
||
# que alguém resolve; um CRM no ar sem isolamento, não.
|
||
REGRAS_FALTANDO="$faltando"
|
||
exit 1
|
||
else
|
||
c_grn "✓ regras de isolamento conferidas ($(printf '%s\n' "$esperadas" | grep -c . ) declaradas, todas no lugar)."
|
||
# ── O BANCO RELIGA AQUI, e nao no fim do script ──────────────────────────
|
||
#
|
||
# MEDIDO na instalacao real em 2026-09-13: as pecas pararam as 03:10:18 e o
|
||
# script so terminou as 03:13:09. QUASE TRES MINUTOS sem o Supabase — e nao
|
||
# por falha: por DESENHO. A volta so acontecia no gatilho de saida, depois
|
||
# de baixar imagem, recriar conteiner e esperar o healthcheck do app.
|
||
#
|
||
# Nada disso precisa do Supabase parado. O que precisava era o DDL, e ele
|
||
# acabou na linha de cima — junto com a conferencia das regras, que e o
|
||
# unico motivo de esperar ate aqui em vez de religar antes.
|
||
#
|
||
# Fica no ramo do SUCESSO de proposito: com regra faltando o script sai no
|
||
# `exit 1` acima, e a volta das pecas vira responsabilidade do gatilho de
|
||
# saida — que religa o banco e deixa o CRM parado, como deve.
|
||
religar_o_supabase
|
||
fi
|
||
else
|
||
c_ylw "⚠ supabase/baseline.sql não encontrado — pulei a parte do banco."
|
||
fi
|
||
# Retentativa não cura estes: a migration NÃO chegou, e seguir daqui trocava o
|
||
# app por cima de um banco pela metade com status 0 — o "deu certo" do cron.
|
||
# DEPOIS da conferência das regras de isolamento, nunca antes: ela recria as que
|
||
# faltam e, se não conseguir, mantém o CRM parado. Sair antes dela deixaria o
|
||
# trap subir o app sem regra — tela vazia para todo mundo.
|
||
[ -z "$BANCO_RESTANTE" ] || die "O banco NÃO terminou limpo e os erros acima repetir não cura: a migration NÃO chegou. A atualização PARA aqui."
|
||
[ -n "${DESKCOMM_AGENT_REPORT:-}" ] && eval "${DESKCOMM_AGENT_REPORT_CMD}" banco
|
||
|
||
# ── 4.5 E-mails de acesso, para quem já estava instalado ────────────────────
|
||
# Só COM o token no ambiente, e por isso duas coisas:
|
||
#
|
||
# - é assim que um clone ANTIGO recebe os e-mails com a marca dele. O
|
||
# `install.sh` dele nunca chamou este passo (ele não existia), e nenhuma
|
||
# atualização toca em config de auth por conta própria;
|
||
# - sem o token, o script imprimiria o passo manual — útil UMA vez, na
|
||
# instalação, e ruído em toda atualização a partir daí. Atualização que
|
||
# resmunga toda vez ensina a ignorar a saída dela.
|
||
#
|
||
# E sem o token, UMA vez na vida: quem instalou antes de a entrevista pedir o
|
||
# token tem o Site URL do projeto em `localhost:3000` — o link de "esqueci minha
|
||
# senha" leva a uma máquina que não existe fora do laptop de quem desenvolve.
|
||
# Esse parque não é alcançado por nada: o `install.sh` dele não perguntou o
|
||
# token, e o bloco acima só roda com token. Sem esta linha, a população
|
||
# REALMENTE quebrada hoje nunca fica sabendo.
|
||
#
|
||
# Uma vez, e nunca mais — o marcador em disco garante isso, que é o que separa
|
||
# um recado de um resmungo mensal. E o texto CONFERE, não acusa: quem já
|
||
# configurou à mão está certo, e ler "seus e-mails estão quebrados" numa
|
||
# atualização que correu bem seria alarme falso na cara de quem fez tudo certo.
|
||
AVISO_SITE_URL=""
|
||
MARCA_AVISO_SITE_URL="$PROJECT_DIR/.deskcomm-site-url-avisado"
|
||
if [ -n "${SUPABASE_ACCESS_TOKEN:-}" ]; then
|
||
bash "$KIT_DIR/marca-emails.sh" --projeto "$PROJECT_DIR" || true
|
||
: > "$MARCA_AVISO_SITE_URL" 2>/dev/null || true # o passo automático rodou
|
||
elif [ ! -e "$MARCA_AVISO_SITE_URL" ]; then
|
||
AVISO_SITE_URL=1
|
||
fi
|
||
|
||
# ── 5. App novo ──────────────────────────────────────────────────────────────
|
||
step "Baixando a versão nova do app e reiniciando"
|
||
# Imagem da TAG publicada (não "latest" solto): garante que o código (checkout
|
||
# acima) e a imagem do container sejam sempre da mesma versão. Gravada no .env,
|
||
# não só exportada: o compose lê a imagem de lá, e um `up -d` rodado à mão
|
||
# depois voltaria pro ":latest" do install — desfazendo a atualização.
|
||
#
|
||
# As TRÊS imagens são gravadas juntas, na mesma versão. O worker e o scheduler
|
||
# passaram a ter imagem publicada porque, antes, eram `build:`-only no compose:
|
||
# `dc pull` os PULAVA ("Skipped - No image to be pulled") e o `dc up -d` abaixo
|
||
# recriava o contêiner sobre a imagem velha, sem `--build`. Resultado: o worker
|
||
# — que é o runtime do agente de IA — ficava congelado no código do dia da
|
||
# instalação e atravessava todas as atualizações. Esta é a linha que conserta
|
||
# isso para o parque já instalado, sem que ninguém precise editar arquivo.
|
||
#
|
||
# `gravar_imagens` também resolve o pull_policy pela mutabilidade da tag: como
|
||
# aqui o alvo é sempre uma tag de versão (imutável), sai `missing`. Isso além de
|
||
# tudo desfaz o "missing" que um rollback anterior deixava para trás — antes ele
|
||
# ficava no .env para sempre, e o `up -d` manual do dono nunca mais puxava nada.
|
||
# Lido ANTES de `gravar_imagens` corrigir — senão a informação some. Este é o
|
||
# estado que a execução ANTERIOR deixou, e o dono nunca soube: o `update.sh`
|
||
# antigo grava só `APP_IMAGE`, e o worker fica seguindo um canal móvel.
|
||
PIN_FALTANDO_ANTES="$(pin_incompleto .env)"
|
||
|
||
VERSAO_ALVO="${TARGET_TAG#v}"
|
||
export APP_IMAGE="${IMG_APP}:${VERSAO_ALVO}"
|
||
export WORKER_IMAGE="${IMG_WORKER}:${VERSAO_ALVO}"
|
||
export SCHEDULER_IMAGE="${IMG_SCHEDULER}:${VERSAO_ALVO}"
|
||
export VOICE_AGENT_IMAGE="${IMG_VOICE_AGENT}:${VERSAO_ALVO}"
|
||
gravar_imagens .env "$VERSAO_ALVO"
|
||
|
||
# Os segredos da chamada de voz (spec 18), para quem instalou antes dela existir.
|
||
# LACUNA apenas — chave presente, mesmo vazia, é decisão de quem opera. Isto NÃO
|
||
# liga a feature: sem `voz` em COMPOSE_PROFILES o serviço nem é criado. O que
|
||
# isto compra é o dia em que o dono QUISER ligar não começar por inventar dois
|
||
# segredos num editor dentro da VPS, que é o passo manual que a doutrina de
|
||
# packaging proíbe.
|
||
VOZ_CRIADA="$(completar_segredos_da_voz .env)" || VOZ_CRIADA=""
|
||
[ -n "$VOZ_CRIADA" ] && c_ylw " (preparei as credenciais da chamada de voz no .env — ela segue DESLIGADA)"
|
||
|
||
# `dc pull` falha se alguma das três imagens ainda não existir no registro — o
|
||
# que acontece numa instalação atualizando para a primeira versão publicada
|
||
# depois desta mudança, ou se um run de publicação quebrou. Nesse caso o compose
|
||
# ainda tem `build:` ao lado do `image:` do worker e do scheduler, então o
|
||
# `up -d` os constrói localmente: pior que puxar, melhor que não atualizar.
|
||
if ! dc pull; then
|
||
# A mensagem distingue os dois casos porque a consequência é oposta, e uma
|
||
# frase tranquilizadora sobre o caso errado é o pior desfecho possível: o
|
||
# `worker` e o `scheduler` têm `build:` ao lado do `image:` e o `up -d` os
|
||
# constrói; o `app` NÃO tem, então se for a imagem dele que falta, o `up -d`
|
||
# falha logo abaixo e a guarda dele constrói a versão aqui — e dizer "sigo
|
||
# assim mesmo" teria sido mentira.
|
||
if dc pull app >/dev/null 2>&1; then
|
||
c_ylw "⚠ Não consegui puxar todas as imagens da versão ${VERSAO_ALVO}."
|
||
c_ylw " A do app veio; o que faltar é construído aqui (mais lento, mesmo resultado)."
|
||
else
|
||
c_ylw "⚠ Não consegui puxar a imagem do APP na versão ${VERSAO_ALVO}."
|
||
c_ylw " Causas comuns: a versão ainda está publicando, ou o pacote está privado no GHCR."
|
||
c_ylw " Vou tentar subir mesmo assim — se falhar, rode de novo em alguns minutos."
|
||
fi
|
||
fi
|
||
# A rede do proxy externo é declarada como EXTERNA no compose: se ela sumiu
|
||
# (um `docker network prune`, ou o `down -v` que o próprio kit ensina como
|
||
# caminho de recomeço), o `up -d` abaixo morre em "network X declared as
|
||
# external, but could not be found" — e este script roda sozinho pelo agent.sh,
|
||
# então ninguém está lendo a tela para decifrar isso. Mesma função do install.sh.
|
||
garantir_rede_do_proxy
|
||
# O `up -d` falha por imagem ausente no disco e, com ele, a atualização inteira:
|
||
# numa VPS de arquitetura diferente da das imagens publicadas o `pull` acima não
|
||
# traz nada, e o `app` — ao contrário do worker e do scheduler — não tem `build:`
|
||
# ao lado do `image:`, então o Compose não tem como construí-lo. Sem esta guarda
|
||
# o script terminava como se tivesse dado certo e o dono ficava na versão velha
|
||
# sem saber; pelo botão "Atualizar" do site, pior: o agente roda sozinho no cron
|
||
# e não há ninguém lendo a tela para desconfiar.
|
||
#
|
||
# O gatilho é o CÓDIGO DE SAÍDA, nunca o texto do erro — arquitetura da VPS, tag
|
||
# ainda publicando, pacote privado no registro e registro fora do ar caem no
|
||
# mesmo caminho, sem depender de casar em inglês uma frase que o Docker muda. O
|
||
# custo é o pior caso: um `up -d` que falhe por outro motivo gasta o build antes
|
||
# de desistir. É o preço de não adivinhar.
|
||
# ⛔ O AVISO DESCE AQUI, e nao no gatilho de saida.
|
||
#
|
||
# MEDIDO na atualizacao real para a v1.17.21: o gatilho roda depois de mais
|
||
# quatro etapas — baixar imagem, recriar, conferir saude, conferir automacoes. E
|
||
# o roteamento do aviso tem prioridade 500, ACIMA da regra do app. Resultado: o
|
||
# CRM voltava ao ar e quem abrisse continuava vendo "estamos atualizando" por
|
||
# minutos, com o sistema ja funcionando. Aviso que mente e pior que aviso nenhum:
|
||
# a pessoa vai embora achando que o sistema esta fora.
|
||
#
|
||
# `restaurar_servicos` segue chamando o mesmo `manutencao_desce` — ele e
|
||
# `docker rm -f ... || true`, idempotente de proposito, e la ele cobre o caminho
|
||
# de ERRO, onde este ponto aqui nunca chega a ser alcancado.
|
||
manutencao_desce
|
||
CONSTRUIU_AQUI=""
|
||
if ! dc up -d; then
|
||
if construir_aqui_e_subir "$VERSAO_ALVO"; then
|
||
CONSTRUIU_AQUI=1
|
||
else
|
||
c_red "✖ A atualização não terminou: nem as imagens prontas desta versão nem a construção aqui funcionaram."
|
||
c_ylw " O CRM segue no ar, na versão anterior. O erro está logo acima;"
|
||
c_ylw " para reproduzir só a construção: docker compose $(dc_files) -f ${COMPOSE_BUILD} build"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# O Caddyfile entra no container por bind mount de UM ARQUIVO, e bind mount de
|
||
# arquivo fica preso ao inode. O `git pull` não edita o arquivo: escreve outro e
|
||
# renomeia, gerando inode novo — o container continua lendo o antigo, para
|
||
# sempre. Medido nesta VPS: host inode 3283869, container 3271833, com o
|
||
# conteúdo velho lá dentro.
|
||
#
|
||
# Sem este force-recreate, TODA mudança de proxy enviada numa atualização
|
||
# (inclusive correção de segurança na borda) some em silêncio: o update diz
|
||
# "concluída" e a configuração antiga segue valendo.
|
||
#
|
||
# Com proxy externo não há Caddy para recriar — e não basta o profile inativo
|
||
# do override: nomear o serviço explicitamente (`up -d ... caddy`) ATIVA o
|
||
# profile dele no Compose e sobe o contêiner assim mesmo, indo bater de frente
|
||
# com o Traefik nas portas 80/443. O resultado era um "⚠ não consegui recriar o
|
||
# proxy" em TODA atualização de quem usa proxy externo: alarme falso, num
|
||
# momento em que o dono precisa confiar no que está lendo.
|
||
case "${REVERSE_PROXY:-caddy}" in
|
||
traefik|npm)
|
||
c_grn "✓ proxy externo (${REVERSE_PROXY}): o Caddy não é usado aqui — nada a recarregar"
|
||
;;
|
||
*)
|
||
dc up -d --force-recreate --no-deps caddy >/dev/null 2>&1 \
|
||
&& c_grn "✓ proxy recarregado com a configuração desta versão" \
|
||
|| c_ylw "⚠ não consegui recriar o proxy — rode: docker compose $(dc_files) up -d --force-recreate caddy"
|
||
;;
|
||
esac
|
||
|
||
# ── 6. O app voltou no ar? ───────────────────────────────────────────────────
|
||
step "Conferindo se o app voltou no ar"
|
||
ok=""
|
||
wait_app_healthy 20 3 >/dev/null && ok=1
|
||
if [ -n "$ok" ]; then
|
||
# O marcador que a guarda de arquitetura lê (#1778). Instalações antigas só
|
||
# seriam reconhecidas pelo contêiner — que um `down` sem `-v` apaga. Gravar
|
||
# aqui, com o app saudável, fecha esse caso a partir desta atualização.
|
||
# Falhar em gravar não desfaz nada: a guarda segue caindo no sinal do
|
||
# contêiner para arquiteturas que ainda não têm imagens publicadas.
|
||
marcar_instalacao_feita "$TARGET_TAG" || true
|
||
if [ -n "$BANCO_INCOMPLETO" ]; then
|
||
c_ylw "⚠ App no ar e saudável, mas o banco NÃO terminou limpo — o que fazer está no fim desta saída."
|
||
else
|
||
c_grn "✓ Atualização concluída — app no ar e saudável."
|
||
# Dito AQUI, no fim, porque é o que sobra na tela do site: o agent.sh manda o
|
||
# rabo da saída, e o build local encheu as linhas de cima com a própria
|
||
# construção. Sem esta frase o dono lê "concluída" e não faz ideia de que a
|
||
# VPS dele passou 20 minutos construindo imagens.
|
||
if [ -n "$CONSTRUIU_AQUI" ]; then
|
||
c_ylw " (as três imagens desta versão foram construídas aqui nesta VPS: as"
|
||
c_ylw " prontas não servem para a arquitetura dela. Toda atualização aqui"
|
||
c_ylw " segue o mesmo caminho — é mais lento e não precisa de nada manual.)"
|
||
fi
|
||
fi
|
||
# Dito no fim, e não no início, porque é aqui que o dono lê. Se a execução
|
||
# anterior deixou o pin pela metade, ele nunca soube — a tela dizia "concluída"
|
||
# e o worker seguia um canal móvel. Agora ele sabe que existiu e que acabou.
|
||
if [ -n "$PIN_FALTANDO_ANTES" ]; then
|
||
c_ylw " (de quebra: a versão de $PIN_FALTANDO_ANTES estava solta e foi fixada agora)"
|
||
fi
|
||
# Dito aqui pelo mesmo motivo do pin: é no fim que o dono lê.
|
||
if [ -n "$AVISO_SITE_URL" ]; then
|
||
DOM_AVISO="$(printf '%s' "${NEXT_PUBLIC_APP_URL:-https://SEU_DOMINIO}")"
|
||
cat <<AVISO
|
||
|
||
$(c_ylw " ─── CONFIRA UMA COISA, UMA VEZ SÓ ─────────────────────")
|
||
|
||
Os e-mails de acesso (esqueci minha senha, confirmação de cadastro,
|
||
aceite de convite) levam para o endereço que estiver em Authentication
|
||
→ URL Configuration, no painel do Supabase. Instalações feitas antes de
|
||
o instalador perguntar o token do Supabase ficaram com o padrão de
|
||
projeto novo, \`http://localhost:3000\`, que só existe na máquina de
|
||
quem desenvolve — e aí ninguém consegue redefinir a própria senha.
|
||
|
||
Vale conferir. Se já estiver com os valores abaixo, não há nada a fazer:
|
||
|
||
Site URL: ${DOM_AVISO}
|
||
Redirect URLs: ${DOM_AVISO%/}/auth/confirm
|
||
|
||
Este aviso não se repete — para o instalador cuidar disso sozinho, rode
|
||
o update com \`export SUPABASE_ACCESS_TOKEN=sbp_...\` no ambiente.
|
||
AVISO
|
||
: > "$MARCA_AVISO_SITE_URL" 2>/dev/null || true
|
||
fi
|
||
else
|
||
c_ylw "⚠ Atualizei, mas o app não respondeu 'ok'. Veja os logs:"
|
||
c_ylw " docker compose $(dc_files) logs --tail=50 app"
|
||
# Código de saída != 0: é o que o agent.sh usa pra saber que precisa voltar
|
||
# pra imagem anterior (guardada por ele ANTES do pull). Sem isso, não existe
|
||
# rede de proteção — o app novo, quebrado, ficaria no ar sem ninguém saber.
|
||
exit 1
|
||
fi
|
||
|
||
# ── 7. Automações (cron do drain de eventos; o da tela já subiu no bloco 0) ──
|
||
step "Conferindo as automações"
|
||
ensure_encryption_key .env
|
||
setup_event_log_drain_cron
|
||
|
||
# ── Fim: o banco que não terminou limpo é a ÚLTIMA coisa na tela ─────────────
|
||
# Na v1.27.3 de uma VPS real o aviso do passo 4 ficou soterrado por centenas de
|
||
# linhas do docker pull, e as últimas linhas da tela eram ✓ verdes. É aqui,
|
||
# depois de tudo, que o dono lê.
|
||
if [ -n "$BANCO_INCOMPLETO" ]; then
|
||
step "Atenção: o banco NÃO terminou limpo nesta atualização"
|
||
c_ylw " Os avisos completos estão no passo \"Atualizando o banco de dados\", acima."
|
||
orientar_banco_incompleto
|
||
fi
|