#!/usr/bin/env bash
#
# Publicar na PRODUÇÃO -- a release montada fora, levada à pasta ao lado, a troca
# das árvores e das pastas, e a volta à liberação anterior (o D37; o item 9 de "O
# que falta fazer antes da janela" em docs/procedimento-de-corte-etapa-1.md).
#
# ESCRITO EM 08/11/2026 E NUNCA RODADO NA PRODUÇÃO NEM EM SERVIDOR NENHUM: só no
# ensaio (scripts/ensaiar-publicar-producao.sh), num contêiner descartável, com
# dublês do nginx, do cron, do MySQL e do PHP-FPM. O dono, ou quem ele indicar,
# lê e roda -- e revisa antes da janela.
#
# Na SUA MÁQUINA, com o repositório e o docker:
#
#     bash scripts/publicar-producao.sh montar <commit> <sobre>
#
#   <commit> é o que vai subir (o que passou nos portões); <sobre> é o que a
#   produção roda HOJE -- na janela da etapa 1, o eaf1fca5 (a importação: a
#   produção não tem git, e o repositório nasceu dela); depois dela, o commit da
#   última release, que o servidor guarda em public_html/.publicar/release.
#   Sai um pacote (release-<curto>.tar.gz e o .sha256) em $RELEASES (padrão:
#   ~/franquiabv-releases), e os comandos de copiá-lo.
#
# NO SERVIDOR, como root, de dentro da manutenção (o roteiro vem no pacote:
# tar -xzf release-<curto>.tar.gz arvore/scripts/publicar-producao.sh -O > /root/publicar-producao.sh):
#
#     bash publicar-producao.sh levar /root/release-<curto>.tar.gz   # a pasta ao lado (o passo 1 da janela)
#     bash publicar-producao.sh trocar                                # as pastas, as árvores e a tela nova (o 7 e o 8)
#     bash publicar-producao.sh entregar                              # o trocar, o PHP-FPM e a conferência (as entregas)
#     bash publicar-producao.sh voltar                                # a liberação anterior de volta
#     bash publicar-producao.sh conferir                              # só confere; não muda nada
#
# Variáveis (no servidor):
#     MANUTENCAO_ARQUIVO=<o arquivo do nginx da página de manutenção>   (obrigatória;
#                         o item 10 de "O que falta": a página NÃO EXISTE ainda)
#     FRONT_RAIZ=<a raiz da tela nova, fora da árvore>   (obrigatória; a do vhost novo)
#     POOL_USUARIO=<usuário do pool do php-fpm>  POOL_GRUPO=<o grupo>   (a leitura 7)
#     PHP_FPM_SERVICO=<o serviço do php-fpm>      (o nome é a medir: a leitura 7)
#     ENTREGA_MIN=<minutos>  VOLTA_MIN=<minutos>  (os do ensaio: a hora-limite sai deles)
#     HORA_LIMITE=01:10                           (a 206 (a); na janela da etapa 1, 05:00)
#     CRONTAB_GUARDADO=<o `crontab -l` DESTA NOITE, guardado antes do crontab -r>
#                         (obrigatória, SEM PADRÃO: na janela da etapa 1, o
#                         /root/crontab-antes-do-corte.txt do passo 1; numa
#                         entrega, OUTRO arquivo -- /root/crontab-entrega-<data>.txt
#                         --, porque o da janela é o que a volta ao código antigo
#                         religa; na volta ao código antigo, o da janela)
#     LOCAL_PHP=<o local.php da release>          (só no `levar` da janela da etapa 1)
#     USUARIO_DB_NO_AR=<o usuário do banco do código antigo no ar>   (obrigatória
#                         enquanto o código no ar é o antigo -- a janela da etapa 1:
#                         a leitura 10, ele pode não ser o do local.php da release)
#     DUMP_USUARIO_DB=<o usuário do dump de hora em hora>   (a leitura 11; opcional)
#     ESPERA_MAX_S=900                            (a espera das rotinas em curso)
#     DIFERENCAS_ACEITAS=<arquivo com um caminho por linha>   (a divergência aceita pelo dono)
#     CONFIRMAR=sim                               (pula as perguntas)
#
# ------------------------------------------------------------------------------
# O QUE ELE GARANTE, E COMO
#
# QUEM CONFERE O QUÊ (a verificação da fatia 129: os documentos diziam "cada um
# recusa" tudo, e não é assim):
#
#                 hora   manutenção   cron   rotinas   impressão
#     levar        sim       --        --      --         --      (o passo 1: vem ANTES da manutenção do 2)
#     trocar       sim      sim       sim     sim        sim
#     entregar     sim      sim       sim     sim        sim
#     conferir     sim      sim       sim     sim*       sim*     (* com a release ao lado)
#     voltar       --       sim       sim     sim         --      (o caminho de sair, a qualquer hora)
#
# 1. A HORA DA 206 (a): "depois das 00:05 e antes das rotinas da 01:10". Fora
#    dela, recusa; e recusa a entrega que, pelo ensaio, não cabe: agora +
#    ENTREGA_MIN > HORA_LIMITE − VOLTA_MIN ("a entrega que, pelo ensaio, não
#    cabe até essa hora não começa").
# 2. A MANUTENÇÃO NO AR: o MANUTENCAO_ARQUIVO existe e o `nginx -T` o carrega.
#    A página não existe (o item 10): sem ela, nada troca.
# 3. O CRON PARADO: o `crontab -l` do root sem linha ativa, o /etc/cron.d sem
#    linha que chame o site, e o CRONTAB_GUARDADO com o crontab DESTA NOITE --
#    o que a entrega religa no fim, e não o passo 12 da janela, que instala o
#    novo (a 194 (a)). Sem padrão, de menos de 12 horas, e, numa entrega, nunca
#    o da janela (/root/crontab-antes-do-corte.txt): é o que a volta ao código
#    antigo religa, e gravar o desta noite por cima dele o perdia. Na volta ao
#    código antigo, ao contrário, só o da janela.
# 4. AS ROTINAS EM CURSO TERMINADAS: espera, até ESPERA_MAX_S, o SHOW
#    PROCESSLIST sem conexão dos usuários da aplicação -- o do local.php da
#    release ao lado E o da árvore no ar, lidos pelo
#    scripts/conexao-do-local-php.php, e, com o código antigo no ar, o
#    USUARIO_DB_NO_AR (a leitura 10: o local.php da release pode trazer outro
#    usuário, e as rotinas antigas em curso não apareciam) -- nem do dump, e
#    nenhum mysqldump/dumpv3 rodando: o sinal do passo 2, e não o relógio.
# 5. A PRODUÇÃO NÃO DIVERGIU (o D37: "Nenhum deploy pode acontecer antes de
#    saber se produção divergiu"): a impressão digital da árvore no ar
#    (scripts/comparar-com-producao.sh) tem de ser a do <sobre>, que o pacote
#    traz; sem o local.php, que não é do git. O que divergir para tudo, com a
#    lista -- salvo os caminhos que o dono aceitou em DIFERENCAS_ACEITAS.
# 6. NÃO POR CIMA: a release vai para public_html.novo, recebe o data/ (cp -aT),
#    as pastas de upload e o avatar (mv -T, no mesmo disco) e o app.json, e só
#    então troca de nome com a árvore no ar, que fica inteira ao lado
#    (public_html.antigo-<data> na primeira vez; public_html.anterior-<hora>
#    depois) para a volta. O passo 7 da etapa 1, na mesma ordem.
# 7. A VOLTA AO CÓDIGO ANTIGO NA ORDEM DE "A volta" do procedimento: só com o
#    bloqueio do migrar-senhas no `nginx -T` (o item 2 dela, o vhost de antes
#    com o bloqueio, vem antes das árvores), e SEM recarregar o PHP-FPM, que vem
#    depois dos nomes das permissões devolvidos (o item 4) -- o roteiro diz o
#    caminho certo do devolver-nomes, na árvore devolvida, que sai com o nome
#    que "A volta" usa (public_html.novo-<data>). A volta entre duas releases
#    recarrega na hora, e a árvore que sai fica em public_html.devolvida-<hora>.
#
# O QUE ELE NÃO FAZ: as migrações (o passo 0 e as da entrega), os roteiros .sql
# do passo 7, o vhost e o bloqueio do migrar-senhas (o 8), o crontab de volta (o
# 12) e liberar (o 13) -- a janela e as entregas os fazem à mão, como o
# procedimento escreve. O `trocar` para antes do PHP-FPM (a janela tem os .sql do
# 7 e o vhost do 8 no meio); o `entregar` recarrega e confere; o `voltar` ao
# código antigo para antes dele (o item 4 de "A volta" vem no meio).
# ------------------------------------------------------------------------------

set -euo pipefail

SUBCOMANDO="${1:-}"

SITE="/var/www/franquiabv.com.br"
ARVORE="$SITE/public_html"
NOVA="$SITE/public_html.novo"
ESTADO="$SITE/.publicar-producao"
DOMINIO="franquiabv.com.br"
PASTAS_DE_UPLOAD="arquivos contratos banners assistentes vip-class visagismo logs"
AVATAR="public/layout/img/avatar"
AVATARES_PADRAO="padrao.png padrao_femi.jpg padrao_masc.jpg"
GRAVAVEIS="data/cache data/logs data/logs/agenda_json data/DoctrineORMModule/Proxy public/layout/img/avatar/cache public/layout/img/avatar/tmp"
IMAGEM_NODE="node:22-bookworm-slim"
IMAGEM_PHP="franquiabv-release-php"

falhar() { printf '\nABORTADO: %s\n' "$*" >&2; exit 1; }
passo()  { printf '\n==> %s\n' "$*"; }
ok()     { printf '    ok  %s\n' "$*"; }
aviso()  { printf '    !!  %s\n' "$*" >&2; }
carimbo() { date +%Y%m%d-%H%M%S; }

confirmar() {
    [ "${CONFIRMAR:-}" = "sim" ] && return 0
    [ -t 0 ] || falhar "sem terminal para confirmar. Rode com CONFIRMAR=sim se é isso mesmo."
    printf '%s (digite: sim) ' "$1"
    local resposta
    read -r resposta
    [ "$resposta" = "sim" ] || falhar "cancelado."
}

# O QUE PAROU NO MEIO FICA DITO: a manutenção segue no ar (ninguém de fora vê o
# meio), e o caminho de sair é o `voltar`, que sabe desfazer as duas metades.
ONDE=""
ao_sair() {
    local status=$?
    if [ "$status" -ne 0 ] && [ -n "$ONDE" ]; then
        printf '\n!!  parou em: %s (saída %s). A manutenção segue no ar. Leia acima, e volte com:\n        bash publicar-producao.sh voltar\n' "$ONDE" "$status" >&2
    fi
    exit "$status"
}
trap ao_sair EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

# ------------------------------------------------------------------------------
# montar: na sua máquina

cmd_montar() {
    local commit="${1:-}" sobre="${2:-}" raiz trabalho curto destino pacote
    [ -n "$commit" ] && [ -n "$sobre" ] || falhar "uso: bash scripts/publicar-producao.sh montar <commit> <sobre>"
    case "$(hostname)" in
        *barbeariavip*|*produc*) falhar "o montar roda na sua máquina, e este servidor ($(hostname)) tem nome de produção." ;;
    esac
    # DE DENTRO DO REPOSITÓRIO, e não de onde o roteiro mora: o ensaio roda uma
    # cópia dele, fora daqui.
    raiz="$(git rev-parse --show-toplevel 2>/dev/null)" || falhar "rode o montar de dentro do repositório."
    cd "$raiz"
    git rev-parse --verify --quiet "$commit^{commit}" >/dev/null || falhar "o commit $commit não existe aqui."
    git rev-parse --verify --quiet "$sobre^{commit}" >/dev/null || falhar "o commit $sobre (o que a produção roda) não existe aqui."
    commit="$(git rev-parse "$commit^{commit}")"
    sobre="$(git rev-parse "$sobre^{commit}")"
    curto="$(git rev-parse --short "$commit")"
    command -v docker >/dev/null 2>&1 || falhar "o montar precisa do docker (o composer e a build da tela nova rodam em contêiner)."

    destino="${RELEASES:-$HOME/franquiabv-releases}"
    mkdir -p "$destino"
    pacote="$destino/release-$curto.tar.gz"
    [ ! -e "$pacote" ] || falhar "$pacote já existe: apague-o, ou use-o."
    trabalho="$(mktemp -d)"
    # shellcheck disable=SC2064
    trap "s=\$?; rm -rf '$trabalho'; (exit \$s); ao_sair" EXIT

    # 1. O CÓDIGO: o git archive do commit, e não a árvore de trabalho de
    #    ninguém -- a produção recebe arquivos, e não o repositório (o histórico
    #    guarda as credenciais antigas, o D3).
    passo "o código de $curto (git archive)"
    mkdir -p "$trabalho/arvore" "$trabalho/sobre"
    git archive "$commit" | tar -x -C "$trabalho/arvore"
    ( cd "$trabalho/arvore" && find . -type f ! -path './vendor/*' -print0 | sort -z | xargs -0 sha256sum ) > "$trabalho/antes.txt"

    # 2. O vendor/, NUMA IMAGEM DO docker/php/Dockerfile (o PHP 8.1 e o Composer
    #    que o laminas-dependency-plugin exige), como o subir_e_ajustar do teste.
    passo "o vendor/ (composer install, na imagem do docker/php/Dockerfile)"
    docker build -q --build-arg PHP_VERSION=8.1 -t "$IMAGEM_PHP" docker/php >/dev/null
    docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -e COMPOSER_HOME=/tmp/composer \
        -v "$trabalho/arvore:/app" -w /app "$IMAGEM_PHP" \
        composer install --no-interaction --prefer-dist --no-progress
    [ -f "$trabalho/arvore/vendor/autoload.php" ] || falhar "o composer não deixou o vendor/autoload.php."
    ( cd "$trabalho/arvore" && find . -type f ! -path './vendor/*' -print0 | sort -z | xargs -0 sha256sum ) > "$trabalho/depois.txt"
    # O MESMO FREIO DO TESTE: o composer não muda arquivo versionado.
    cmp -s "$trabalho/antes.txt" "$trabalho/depois.txt" \
        || falhar "o composer mudou arquivo fora do vendor/ (plugin?): $(diff "$trabalho/antes.txt" "$trabalho/depois.txt" | head -n 5)"

    # 3. A TELA NOVA, num contêiner Node (a produção não precisa de Node: só o
    #    dist viaja), com --base=/novo/ -- a construir_front do teste.
    passo "a tela nova (--base=/novo/) num contêiner $IMAGEM_NODE"
    docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -e npm_config_cache=/tmp/npm \
        -v "$trabalho/arvore/frontend:/app" -w /app "$IMAGEM_NODE" \
        sh -c 'npm ci --no-audit --no-fund && npm run build -- --base=/novo/'
    grep -q 'src="/novo/assets/' "$trabalho/arvore/frontend/dist/index.html" 2>/dev/null \
        || falhar "a build não saiu com --base=/novo/."
    mkdir -p "$trabalho/arvore/.publicar"
    mv "$trabalho/arvore/frontend/dist" "$trabalho/arvore/.publicar/novo"
    rm -rf "$trabalho/arvore/frontend/node_modules"

    # 4. O QUE A PRODUÇÃO RODA HOJE, em impressão digital: o servidor confere a
    #    árvore no ar contra ela antes de mexer (o D37).
    passo "a impressão digital de $(git rev-parse --short "$sobre"), o que a produção roda"
    git archive "$sobre" | tar -x -C "$trabalho/sobre"
    RAIZ="$trabalho/sobre" sh scripts/comparar-com-producao.sh > "$trabalho/arvore/.publicar/impressao-esperada.txt"
    grep -qE '^[0-9a-f]{64}  module/' "$trabalho/arvore/.publicar/impressao-esperada.txt" \
        || falhar "a impressão de $sobre saiu sem nenhum arquivo de module/."

    printf '%s\n' "$commit" > "$trabalho/arvore/.publicar/release"
    printf '%s\n' "$sobre" > "$trabalho/arvore/.publicar/sobre"
    printf '%s %s\n' "$(date -Iseconds)" "$(git log -1 --format=%s "$commit" | cut -c1-120)" > "$trabalho/arvore/.publicar/montado"

    # NADA DA ÁRVORE DE TRABALHO: as cópias anonimizadas e os local.php dos
    # ensaios ficam fora do git, e não de uma cópia de pasta.
    local intruso
    intruso="$(cd "$trabalho/arvore" && find . \( -name 'base-teste*.sql.gz' -o -path './ensaios' -o -path './config/autoload/local.php' -o -path './.env' \) -print | head -n 3)"
    [ -z "$intruso" ] || falhar "o pacote levaria o que não pode: $intruso"

    passo "o pacote"
    # SEM OS METADADOS DO MAC (os ._ e os atributos estendidos), que o tar do
    # servidor leria como arquivos ou avisos.
    COPYFILE_DISABLE=1 tar -C "$trabalho" -czf "$pacote" arvore
    ( cd "$destino" && sha256sum "release-$curto.tar.gz" > "release-$curto.tar.gz.sha256" )
    cat <<FIM

Pacote: $pacote ($(du -h "$pacote" | cut -f1)), do commit $curto, sobre $(git rev-parse --short "$sobre").

No começo da janela (a sua 201 (a): nada muda no servidor antes dela):
    scp $pacote $pacote.sha256 root@<a produção>:/root/
    scp <o local.php da release, conferido fora do servidor> root@<a produção>:/root/local.php-da-release   (só na etapa 1)
e no servidor, como root:
    tar -xzf /root/release-$curto.tar.gz arvore/scripts/publicar-producao.sh -O > /root/publicar-producao.sh
    bash /root/publicar-producao.sh levar /root/release-$curto.tar.gz
FIM
}

# ------------------------------------------------------------------------------
# as conferências do servidor

minutos() { local h="${1%%:*}" m="${1##*:}"; printf '%d' "$((10#$h * 60 + 10#$m))"; }
agora_em_minutos() { local hm; hm="$(date +%H%M)"; minutos "${hm:0:2}:${hm:2:2}"; }

ser_root() {
    [ "$(id -u)" = 0 ] || falhar "rode como root (o dono das pastas é o do pool, e o nginx e o php-fpm são do sistema)."
    [ -d "$ARVORE" ] || falhar "$ARVORE não existe: este não é o servidor do site."
}

# A HORA DA 206 (a). Sem os números do ensaio, não há hora-limite: recusa.
conferir_a_hora() {
    local agora limite entrega volta
    case "${ENTREGA_MIN:-}${VOLTA_MIN:-}" in
        *[!0-9]*|'') falhar "ENTREGA_MIN e VOLTA_MIN, em minutos, os do ensaio do roteiro (scripts/ensaiar-publicar-producao.sh e o do BVIP, a sua 199 (a)): a hora-limite sai deles." ;;
    esac
    [ -n "${ENTREGA_MIN:-}" ] && [ -n "${VOLTA_MIN:-}" ] || falhar "ENTREGA_MIN e VOLTA_MIN, os dois."
    case "${HORA_LIMITE:=01:10}" in
        [0-2][0-9]:[0-5][0-9]) ;;
        *) falhar "HORA_LIMITE='$HORA_LIMITE': HH:MM." ;;
    esac
    agora="$(agora_em_minutos)"
    limite="$(minutos "$HORA_LIMITE")"
    entrega="$ENTREGA_MIN"
    volta="$VOLTA_MIN"
    [ "$agora" -ge 5 ] && [ "$agora" -le "$limite" ] \
        || falhar "agora são $(date +%H:%M): a entrega é depois das 00:05 e antes das $HORA_LIMITE (a sua 206 (a); a janela da etapa 1, a 190 (a), com HORA_LIMITE=05:00)."
    [ "$((agora + entrega))" -le "$((limite - volta))" ] \
        || falhar "não cabe: agora $(date +%H:%M), a entrega leva $entrega min e a volta $volta min pelo ensaio, e tudo tem de estar liberado ou de volta antes das $HORA_LIMITE. A entrega fica para outra noite (procedimento, \"Depois da janela\")."
    ok "hora: $(date +%H:%M), cabe a entrega ($entrega min) e a volta ($volta min) antes das $HORA_LIMITE"
}

# A MANUTENÇÃO NO AR, MEDIDA NO QUE O NGINX CARREGA -- e não no arquivo em disco.
conferir_a_manutencao() {
    [ -n "${MANUTENCAO_ARQUIVO:-}" ] \
        || falhar "sem MANUTENCAO_ARQUIVO: a página de manutenção (o item 10 de \"O que falta\", a sua 190 (a)) não existe ainda. Sem ela, nada troca."
    [ -f "$MANUTENCAO_ARQUIVO" ] || falhar "$MANUTENCAO_ARQUIVO não existe."
    local conf
    conf="$(nginx -T 2>/dev/null)" || falhar "o nginx -T reprovou: não consigo medir o que ele carrega."
    grep -qF "# configuration file $MANUTENCAO_ARQUIVO:" <<<"$conf" \
        || falhar "o nginx não carrega $MANUTENCAO_ARQUIVO: a manutenção não está no ar (o passo 2)."
    ok "manutenção: $MANUTENCAO_ARQUIVO carregado pelo nginx"
}

# A LINHA DE CRONTAB QUE A RECUSA MOSTRA, SEM O SEGREDO: o token na URL
# (token=...) da forma antiga, e o cabeçalho da forma nova do passo 9 do
# procedimento (-H "Authorization: Bearer <token>", a resposta 5 (b)), que o sed
# de antes deixava passar inteiro para o terminal e para quem grava a sessão
# (a verificação da fatia 129).
sem_segredo() {
    head -n 1 | sed -E \
        -e 's/(token=)[^"&[:space:]]*/\1<tirado>/g' \
        -e 's/([Aa]uthorization:)[^"'"'"']*/\1 <tirado>/g' \
        -e 's/([Bb]earer)[[:space:]]+[^"'"'"'[:space:]]*/\1 <tirado>/g'
}

mesmo_arquivo() { [ "$(readlink -f -- "$1")" = "$(readlink -f -- "$2")" ]; }

# O CRONTAB DA JANELA DA ETAPA 1 (o passo 1 do procedimento): o que a volta ao
# código antigo religa, inteiro ("A volta", o item 5).
CRONTAB_DA_JANELA="/root/crontab-antes-do-corte.txt"

# O CRON PARADO, E O DESTA NOITE GUARDADO.
#
#     conferir_o_cron ida               o trocar, o entregar, o conferir, e a
#                                       volta entre duas releases
#     conferir_o_cron volta-ao-antigo   a volta que deixa o código antigo no ar
#
# O GUARDADO SEM PADRÃO (a verificação da fatia 129): o padrão era o arquivo da
# janela, e numa entrega do dia seguinte -- o operador guardando o desta noite
# noutro nome, como o procedimento manda -- a conferência passava com o de
# antes do corte (o token velho, as quatro rotinas que saíram do código), e o
# fim do entregar mandava reinstalá-lo: o recuo que a 5 tirou. E quem seguisse
# o cabeçalho de antes gravava o desta noite POR CIMA do da janela, que a volta
# ao código antigo precisa até a declaração da 198 (a).
conferir_o_cron() {
    local para="$1" ativo guardado="${CRONTAB_GUARDADO:-}"
    [ -n "$guardado" ] || falhar "CRONTAB_GUARDADO: o arquivo com o crontab -l desta noite, guardado antes do crontab -r (sem padrão: na janela da etapa 1, $CRONTAB_DA_JANELA; numa entrega, outro arquivo, como /root/crontab-entrega-$(date +%Y%m%d).txt; na volta ao código antigo, $CRONTAB_DA_JANELA)."
    ativo="$(crontab -l 2>/dev/null | grep -vE '^[[:space:]]*(#|$)' || true)"
    [ -z "$ativo" ] || falhar "o crontab do root ainda tem linha ativa (o passo 1: crontab -l > $guardado; crontab -r): $(printf '%s\n' "$ativo" | sem_segredo)"
    ativo="$(grep -D skip -RhiE "$DOMINIO|cronjob|/api/marketing" /etc/cron.d 2>/dev/null | grep -vE '^[[:space:]]*(#|$)' || true)"
    [ -z "$ativo" ] || falhar "o /etc/cron.d ainda chama o site (os outros agendadores da leitura 11): $(printf '%s\n' "$ativo" | sem_segredo)"
    grep -qvE '^[[:space:]]*(#|$)' "$guardado" 2>/dev/null \
        || falhar "$guardado não existe ou está vazio: é o crontab que volta no fim. Guarde-o antes do crontab -r."
    if [ "$para" = volta-ao-antigo ]; then
        mesmo_arquivo "$guardado" "$CRONTAB_DA_JANELA" \
            || falhar "a volta deixa o código antigo no ar, e o crontab dele é o da janela ($CRONTAB_DA_JANELA, o de antes, inteiro: o item 5 de \"A volta\" do procedimento), e não $guardado."
        ok "cron: parado; o que volta com o código antigo é o da janela, $guardado"
        return 0
    fi
    [ -n "$(find "$guardado" -maxdepth 0 -mmin -720 2>/dev/null)" ] \
        || falhar "$guardado tem mais de 12 horas: não é o crontab -l desta noite. Guarde o de agora (crontab -l > <arquivo>, antes do crontab -r) e aponte CRONTAB_GUARDADO para ele."
    if [ -f "$ARVORE/.publicar/release" ] && mesmo_arquivo "$guardado" "$CRONTAB_DA_JANELA"; then
        falhar "$guardado é o arquivo da janela da etapa 1, e esta é uma entrega depois dela: ele é o crontab que a volta ao código antigo religa, e o desta noite vai noutro (crontab -l > /root/crontab-entrega-$(date +%Y%m%d).txt). Se você acabou de gravar o desta noite nele, o de antes do corte se perdeu: PARE e diga ao dono, que a volta ao código antigo depende dele."
    fi
    ok "cron: parado, e o desta noite guardado em $guardado"
}

# AS ROTINAS QUE JÁ DISPARARAM: o sinal do passo 2, esperado.
#
#     conferir_as_rotinas <a árvore cujo scripts/conexao-do-local-php.php lê>
#
# DE QUEM SÃO AS CONEXÕES (a verificação da fatia 129): só o usuário do
# local.php da release ao lado era contado, e é o código NO AR que tem rotina em
# curso. Na janela da etapa 1 o código no ar é o antigo, e a leitura 10 diz que
# o local.php da release pode trazer outro usuário (o franquiabv_user que
# estava comentado): a rotina antiga de 590 s seguia, a conta dava 0, e o
# trocar passava. Agora contam o da release ao lado, o do local.php da árvore no
# ar, e -- com o código antigo no ar, que pode conectar com credenciais escritas
# fora do local.php -- o USUARIO_DB_NO_AR, obrigatório.
conferir_as_rotinas() {
    local leitor="$1/scripts/conexao-do-local-php.php" f conexao u usuarios="" lista i conta dump max="${ESPERA_MAX_S:-900}"
    local -a vistos=()
    for f in "$NOVA/config/autoload/local.php" "$ARVORE/config/autoload/local.php"; do
        [ -f "$f" ] || continue
        conexao="$(php "$leitor" "$f")" || falhar "não li a conexão de $f."
        usuarios="$usuarios ${conexao##*|}"
    done
    if [ ! -f "$ARVORE/.publicar/release" ]; then
        [ -n "${USUARIO_DB_NO_AR:-}" ] \
            || falhar "USUARIO_DB_NO_AR: o usuário do banco com que o código antigo, no ar, conecta. A leitura 10: ele pode não ser o do local.php da release, e sem ele a espera não vê as rotinas antigas em curso."
    fi
    usuarios="$usuarios ${USUARIO_DB_NO_AR:-} ${DUMP_USUARIO_DB:-}"
    for u in $usuarios; do
        case "$u" in *[!A-Za-z0-9_.-]*) falhar "o usuário do banco '$u' não é um nome simples." ;; esac
        case " ${vistos[*]-} " in *" $u "*) continue ;; esac
        vistos+=("$u")
    done
    [ "${#vistos[@]}" -gt 0 ] || falhar "nenhum usuário do banco a esperar: sem local.php ao lado nem no ar."
    lista="$(printf "'%s'," "${vistos[@]}")"
    lista="${lista%,}"
    passo "esperando as rotinas em curso (até $max s): nenhuma conexão de ${vistos[*]}, nenhum dump"
    for i in $(seq 0 5 "$max"); do
        conta="$(mysql -N -B -e "SELECT COUNT(*) FROM information_schema.PROCESSLIST WHERE USER IN ($lista)" 2>/dev/null)" \
            || falhar "o mysql não respondeu: não consigo medir as rotinas em curso."
        dump="$(pgrep -af '[m]ysqldump|[d]umpv3' 2>/dev/null || true)"
        if [ "$conta" = 0 ] && [ -z "$dump" ]; then
            ok "rotinas: nenhuma conexão da aplicação nem do dump, e nenhum dump rodando (esperou ${i} s)"
            return 0
        fi
        [ "$i" -ge "$max" ] || sleep 5
    done
    falhar "depois de $max s ainda há $conta conexão(ões) da aplicação ou do dump${dump:+, e um dump rodando}: as rotinas em curso não terminaram. Espere, e rode de novo (o passo 2: espera-se o sinal, e não o relógio)."
}

# A PRODUÇÃO CONTRA O QUE ELA DEVIA SER (o D37).
conferir_a_impressao() {
    local esperada="$NOVA/.publicar/impressao-esperada.txt" medida diferencas aceitas="" fora
    [ -f "$esperada" ] || falhar "a release em $NOVA não traz a impressão esperada (.publicar/impressao-esperada.txt): não é do montar."
    local sobre no_ar
    sobre="$(cat "$NOVA/.publicar/sobre")"
    if [ -f "$ARVORE/.publicar/release" ]; then
        no_ar="$(cat "$ARVORE/.publicar/release")"
        [ "$no_ar" = "$sobre" ] || falhar "a produção roda a release $no_ar, e o pacote foi montado sobre $sobre: monte de novo com <sobre> = $no_ar."
    fi
    medida="$(mktemp)"
    RAIZ="$ARVORE" sh "$NOVA/scripts/comparar-com-producao.sh" > "$medida" || { rm -f "$medida"; falhar "o comparar-com-producao.sh falhou na árvore no ar."; }
    # Só as linhas de arquivo, sem o local.php (fora do git, de propósito); o
    # caminho é o que vem depois do resumo e dos dois espaços.
    diferencas="$(awk '
        !/^[0-9a-f]{64}  / { next }
        { c = substr($0, 67) }
        c ~ /^config\/autoload\/([^\/]*\.)?local\.php$/ { next }
        FNR == NR { e[c] = substr($0, 1, 64); next }
        { m[c] = substr($0, 1, 64) }
        END {
            for (c in e) if (!(c in m)) print "só no repositório: " c; else if (e[c] != m[c]) print "diferente: " c
            for (c in m) if (!(c in e)) print "só no servidor: " c
        }' "$esperada" "$medida" | sort)"
    rm -f "$medida"
    if [ -z "$diferencas" ]; then
        ok "impressão digital: a árvore no ar é a de $(printf '%s' "$sobre" | cut -c1-8)"
        return 0
    fi
    [ -z "${DIFERENCAS_ACEITAS:-}" ] || aceitas="$(grep -vE '^[[:space:]]*(#|$)' "$DIFERENCAS_ACEITAS" 2>/dev/null || true)"
    fora="$(printf '%s\n' "$diferencas" | while IFS= read -r linha; do
                grep -qxF "${linha#*: }" <<<"$aceitas" || printf '%s\n' "$linha"
            done)"
    if [ -n "$fora" ]; then
        falhar "a produção divergiu do que devia rodar ($(printf '%s' "$sobre" | cut -c1-8)) -- o D37: uma mudança feita direto no servidor sumiria na troca, calada. Os caminhos:
$(printf '%s\n' "$fora" | head -n 40)
Traga a mudança ao repositório e monte de novo; ou, se o dono aceitar perdê-la, escreva o caminho num arquivo e rode com DIFERENCAS_ACEITAS=<arquivo>."
    fi
    aviso "impressão digital: $(printf '%s\n' "$diferencas" | wc -l | tr -d ' ') divergência(s) aceitas pelo dono em $DIFERENCAS_ACEITAS"
}

# ------------------------------------------------------------------------------
# levar: a pasta ao lado

cmd_levar() {
    local pacote="${1:-}" livre tamanho entrando
    ser_root
    conferir_a_hora
    [ -n "$pacote" ] && [ -f "$pacote" ] || falhar "informe o pacote: bash publicar-producao.sh levar /root/release-<curto>.tar.gz"
    [ -f "$pacote.sha256" ] || falhar "sem $pacote.sha256: o pacote não se confere."
    [ "$(sha256sum < "$pacote" | cut -c1-64)" = "$(cut -c1-64 "$pacote.sha256")" ] \
        || falhar "o sha256 de $pacote não é o do .sha256: a cópia veio quebrada."
    [ ! -e "$NOVA" ] || falhar "$NOVA já existe (uma release levada antes?). Use-a com o trocar, ou apague-a."
    tamanho="$(stat -c %s "$pacote")"
    livre="$(df -P -B1 "$SITE" | awk 'NR == 2 {print $4}')"
    [ "$livre" -gt "$((tamanho * 4))" ] || falhar "pouco disco em $SITE: $livre bytes livres para um pacote de $tamanho (comprimido)."

    ONDE="levar: desempacotando"
    mkdir -p "$ESTADO"
    chmod 700 "$ESTADO"
    entrando="$ESTADO/entrando-$(carimbo)"
    mkdir "$entrando"
    # O DONO É O ROOT, e não o número de quem montou o pacote na outra máquina.
    tar --no-same-owner -xzf "$pacote" -C "$entrando"
    [ -f "$entrando/arvore/.publicar/release" ] && [ -f "$entrando/arvore/.publicar/novo/index.html" ] \
        || falhar "o pacote não é do montar (sem .publicar/release ou sem a tela nova)."
    mv -T "$entrando/arvore" "$NOVA"
    rmdir "$entrando"

    ONDE="levar: o local.php"
    if [ -n "${LOCAL_PHP:-}" ]; then
        [ -n "${POOL_GRUPO:-}" ] || falhar "POOL_GRUPO: o grupo do pool do php-fpm, que lê o local.php (a leitura 7)."
        install -o root -g "$POOL_GRUPO" -m 640 "$LOCAL_PHP" "$NOVA/config/autoload/local.php"
    elif [ -f "$ARVORE/config/autoload/local.php" ]; then
        cp -a "$ARVORE/config/autoload/local.php" "$NOVA/config/autoload/local.php"
    else
        falhar "sem local.php: a árvore no ar não tem um, e LOCAL_PHP não foi dado (o item 4 de \"pronto\")."
    fi
    ONDE=""
    ok "release $(cut -c1-8 "$NOVA/.publicar/release") em $NOVA, com o local.php; o site não a serve"
}

# ------------------------------------------------------------------------------
# trocar: as pastas, as árvores e a tela nova

# O QUE O VOLTAR PRECISA SABER: em que metade parou.
marcar_passo() { printf '%s %s\n' "$(date -Iseconds)" "$1" > "$ESTADO/passo"; }

conferir_antes_de_trocar() {
    ser_root
    [ -d "$NOVA" ] && [ -f "$NOVA/.publicar/release" ] || falhar "sem a release ao lado ($NOVA): rode o levar."
    [ -f "$NOVA/config/autoload/local.php" ] || falhar "a release ao lado não tem o local.php: rode o levar de novo."
    [ ! -f "$ESTADO/passo" ] || falhar "uma troca anterior não terminou ($(cat "$ESTADO/passo")): rode o voltar, ou termine-a, antes."
    case "${FRONT_RAIZ:-}" in
        "$SITE"/?*) ;;
        *) falhar "FRONT_RAIZ='${FRONT_RAIZ:-}': a raiz da tela nova, embaixo de $SITE/ e fora da árvore (a do vhost novo, o item 5 de \"pronto\")." ;;
    esac
    case "$FRONT_RAIZ" in "$ARVORE"|"$ARVORE"/*|"$NOVA"|"$NOVA"/*|*[!A-Za-z0-9._/-]*|*/../*|*/..) falhar "FRONT_RAIZ='$FRONT_RAIZ' dentro de uma árvore, ou com caracteres fora do simples." ;; esac
    [ -n "${POOL_USUARIO:-}" ] && [ -n "${POOL_GRUPO:-}" ] || falhar "POOL_USUARIO e POOL_GRUPO: o dono das pastas graváveis (a leitura 7)."
    id -u "$POOL_USUARIO" >/dev/null 2>&1 || falhar "o usuário $POOL_USUARIO não existe aqui."
    conferir_a_hora
    conferir_a_manutencao
    conferir_o_cron ida
    conferir_as_rotinas "$NOVA"
    conferir_a_impressao
}

trocar() {
    local p anterior quando
    quando="$(carimbo)"

    passo "o que a release não traz: o data/, as pastas de upload, o avatar e o app.json"
    marcar_passo pastas
    ONDE="trocar: o data/"
    if [ -d "$ARVORE/data" ]; then
        cp -aT "$ARVORE/data" "$NOVA/data"
    fi
    rm -rf "$NOVA/data/DoctrineORMModule/Proxy"
    ONDE="trocar: as pastas de upload"
    for p in $PASTAS_DE_UPLOAD; do
        [ -e "$ARVORE/public/$p" ] || { ok "public/$p não existe lá"; continue; }
        mv -T "$ARVORE/public/$p" "$NOVA/public/$p"
    done
    ONDE="trocar: o avatar"
    if [ -d "$ARVORE/$AVATAR" ]; then
        mv -T "$NOVA/$AVATAR" "$ESTADO/avatar-da-release-$quando"
        mv -T "$ARVORE/$AVATAR" "$NOVA/$AVATAR"
        for p in $AVATARES_PADRAO; do
            cmp -s "$ESTADO/avatar-da-release-$quando/$p" "$NOVA/$AVATAR/$p" \
                || aviso "o $AVATAR/$p de lá difere do da release: fica o de lá (o que o sistema mostra hoje), e a diferença vai ao D37."
        done
    fi
    ONDE="trocar: o app.json"
    [ ! -f "$ARVORE/public/app.json" ] || cp -a "$ARVORE/public/app.json" "$NOVA/public/app.json"
    ONDE="trocar: as pastas graváveis"
    for p in $GRAVAVEIS; do
        if [ ! -d "$NOVA/$p" ]; then
            mkdir -p "$NOVA/$p"
            chown "$POOL_USUARIO:$POOL_GRUPO" "$NOVA/$p"
        fi
    done
    chown root:"$POOL_GRUPO" "$NOVA/config/autoload/local.php"
    chmod 640 "$NOVA/config/autoload/local.php"
    ok "data/ copiado, pastas movidas com mv -T, app.json de lá, graváveis do $POOL_USUARIO"

    passo "a troca das árvores"
    if [ -f "$ARVORE/.publicar/release" ]; then
        anterior="$SITE/public_html.anterior-$quando"
    else
        anterior="$SITE/public_html.antigo-${quando%%-*}"
    fi
    [ ! -e "$anterior" ] || falhar "$anterior já existe."
    ONDE="trocar: as árvores"
    printf '%s\n' "$anterior" > "$ESTADO/anterior"
    mv -T "$ARVORE" "$anterior"
    if ! mv -T "$NOVA" "$ARVORE"; then
        mv -T "$anterior" "$ARVORE"
        falhar "a release não entrou no lugar; a árvore de antes voltou (as pastas seguem na release: rode o voltar)."
    fi
    marcar_passo trocada
    printf '%s entregue %s anterior=%s\n' "$quando" "$(cat "$ARVORE/.publicar/release")" "$anterior" >> "$ESTADO/historico"
    ok "no lugar: $(cut -c1-8 "$ARVORE/.publicar/release"); a de antes, inteira, em $anterior"

    passo "a tela nova em $FRONT_RAIZ/novo (troca atômica)"
    ONDE="trocar: a tela nova"
    mkdir -p "$FRONT_RAIZ"
    rm -rf "$FRONT_RAIZ/novo.entrando" "$FRONT_RAIZ/novo.anterior"
    cp -a "$ARVORE/.publicar/novo" "$FRONT_RAIZ/novo.entrando"
    chmod -R a+rX "$FRONT_RAIZ/novo.entrando"
    [ ! -d "$FRONT_RAIZ/novo" ] || mv -T "$FRONT_RAIZ/novo" "$FRONT_RAIZ/novo.anterior"
    mv -T "$FRONT_RAIZ/novo.entrando" "$FRONT_RAIZ/novo"
    marcar_passo feita
    ONDE=""
    ok "tela nova no lugar; a anterior, se havia, em $FRONT_RAIZ/novo.anterior"
}

cmd_trocar() {
    conferir_antes_de_trocar
    confirmar "Trocar a árvore de $DOMINIO pela release $(cut -c1-8 "$NOVA/.publicar/release")?"
    trocar
    rm -f "$ESTADO/passo"
    cat <<FIM

Trocado. O PHP-FPM NÃO foi recarregado: na janela da etapa 1, seguem à mão os
roteiros .sql do passo 7 (pelo caminho da árvore no lugar), o vhost e o bloqueio
do passo 8, e o recarregar do 9. Para desfazer: bash publicar-producao.sh voltar
FIM
}

recarregar_o_php_fpm() {
    [ -n "${PHP_FPM_SERVICO:-}" ] || falhar "PHP_FPM_SERVICO: o nome do serviço do php-fpm (a leitura 7)."
    passo "recarregando o $PHP_FPM_SERVICO (o OPcache não serve as duas árvores juntas)"
    systemctl reload "$PHP_FPM_SERVICO"
}

# A CONFERÊNCIA SEM ESCREVER, do servidor, por dentro da manutenção (o passo 10).
conferir_a_release_no_ar() {
    local c n
    php "$ARVORE/scripts/verificar-local-php.php" || falhar "o verificar-local-php.php reprovou o local.php da release (acima). PARE: volte com bash publicar-producao.sh voltar"
    c="$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 "https://$DOMINIO/api/cronjobs/npsContabilizar" || echo 000)"
    case "$c" in
        401) ok "o npsContabilizar sem token: 401 (o código novo)" ;;
        503) falhar "o npsContabilizar respondeu 503: é a manutenção, que não deixou o servidor passar. Conserte a passagem e rode o conferir (não é motivo de volta)." ;;
        *) falhar "o npsContabilizar sem token respondeu $c, e o código novo dá 401. PARE: volte com bash publicar-producao.sh voltar" ;;
    esac
    n="$(curl -s --max-time 30 "https://$DOMINIO/novo/" | grep -c 'src="/novo/assets/' || true)"
    [ "${n:-0}" -ge 1 ] || falhar "o /novo/ não trouxe a build (nem um src=\"/novo/assets/\"). PARE: volte com bash publicar-producao.sh voltar"
    ok "o /novo/ com a build"
}

cmd_entregar() {
    local inicio
    inicio="$(date +%s)"
    # O SERVIÇO ANTES DE TROCAR: sem ele, a troca acontecia e o recarregar falhava depois.
    [ -n "${PHP_FPM_SERVICO:-}" ] || falhar "PHP_FPM_SERVICO: o nome do serviço do php-fpm (a leitura 7)."
    systemctl cat "$PHP_FPM_SERVICO" >/dev/null 2>&1 || falhar "o serviço $PHP_FPM_SERVICO não existe aqui (systemctl cat)."
    conferir_antes_de_trocar
    confirmar "Entregar a release $(cut -c1-8 "$NOVA/.publicar/release") em $DOMINIO?"
    trocar
    ONDE="entregar: o PHP-FPM"
    recarregar_o_php_fpm
    ONDE="entregar: a conferência"
    conferir_a_release_no_ar
    rm -f "$ESTADO/passo"
    ONDE=""
    printf '\nEntregue: %s, em %s s. Falta, à mão: o crontab desta noite de volta (crontab %s; se a release muda as rotinas, o que o procedimento da entrega disser) e liberar (tirar a manutenção).\n' \
        "$(cut -c1-8 "$ARVORE/.publicar/release")" "$(( $(date +%s) - inicio ))" "$CRONTAB_GUARDADO"
}

# ------------------------------------------------------------------------------
# voltar: a liberação anterior

# A VOLTA AO CÓDIGO ANTIGO SÓ COM O BLOQUEIO DO migrar-senhas CARREGADO: o item 2
# de "A volta" do procedimento (o vhost de antes, COM o bloqueio) vem antes das
# árvores, e o código antigo tem a rota aberta. Medido no que o nginx carrega, como
# o próprio item 2 manda (nginx -T | grep migrar-senhas); sem curl nessa rota.
conferir_o_bloqueio() {
    local conf
    conf="$(nginx -T 2>/dev/null)" || falhar "o nginx -T reprovou: não consigo medir se o bloqueio do migrar-senhas está carregado."
    grep -q 'migrar-senhas' <<<"$conf" \
        || falhar "o nginx não carrega o bloqueio do migrar-senhas, e esta volta põe no ar o código antigo, que tem a rota aberta. Faça antes o item 2 de \"A volta\" do procedimento (o vhost de antes, COM o bloqueio: scripts/bloquear-migrar-senhas.sh --aplicar, nginx -t, reload) e rode de novo."
    ok "o bloqueio do migrar-senhas carregado pelo nginx"
}

cmd_voltar() {
    local anterior devolvida p estado inicio fica para
    inicio="$(date +%s)"
    ser_root
    estado="$(cut -d' ' -f2 "$ESTADO/passo" 2>/dev/null || true)"
    anterior="$(cat "$ESTADO/anterior" 2>/dev/null || true)"
    [ "$estado" != voltando ] || falhar "uma volta anterior parou no meio ($(cat "$ESTADO/passo")): olhe à mão as árvores em $SITE e o $ESTADO/historico."
    # O QUE FICA NO AR DEPOIS DA VOLTA decide o crontab que volta e a ordem: o
    # código antigo (sem .publicar/release) ou uma release.
    if [ "$estado" = pastas ]; then fica="$ARVORE"; else fica="$anterior"; fi
    if [ -n "$fica" ] && [ ! -f "$fica/.publicar/release" ]; then para=volta-ao-antigo; else para=ida; fi
    # SEM HORA E SEM IMPRESSÃO: a volta é o caminho de sair, a qualquer hora. Com
    # a manutenção, o cron parado e as rotinas terminadas, como a ida.
    conferir_a_manutencao
    conferir_o_cron "$para"

    # A METADE DE ANTES DA TROCA: as pastas foram para a release ao lado, e a
    # árvore no ar ficou sem elas. Devolve e para.
    if [ "$estado" = pastas ]; then
        [ -d "$NOVA" ] || falhar "a troca parou nas pastas, e $NOVA não existe: olhe à mão."
        conferir_as_rotinas "$NOVA"
        confirmar "Devolver as pastas da release ao lado para a árvore no ar?"
        ONDE="voltar: as pastas, de antes da troca"
        devolver_as_pastas "$NOVA" "$ARVORE" sem-app-json
        rm -f "$ESTADO/passo" "$ESTADO/anterior"
        printf '\nAs pastas voltaram à árvore no ar; a release ao lado ficou em %s (apague-a, ou leve outra). Falta, à mão: o crontab de volta (crontab %s) e liberar.\n' "$NOVA" "$CRONTAB_GUARDADO"
        ONDE=""
        return 0
    fi

    [ -n "$anterior" ] && [ -d "$anterior" ] || falhar "não há liberação anterior registrada em $ESTADO/anterior, ou ela não existe mais."
    conferir_as_rotinas "$ARVORE"
    [ -n "${FRONT_RAIZ:-}" ] || falhar "FRONT_RAIZ: a mesma da entrega."
    if [ "$para" = volta-ao-antigo ]; then
        conferir_o_bloqueio
    else
        [ -n "${PHP_FPM_SERVICO:-}" ] || falhar "PHP_FPM_SERVICO: o nome do serviço do php-fpm (a leitura 7), que a volta entre duas releases recarrega."
    fi
    # O NOME DA ÁRVORE QUE SAI: na volta ao código antigo, o de "A volta" do
    # procedimento (public_html.novo-<data>), que o item 4 dela usa para o
    # devolver-nomes; entre duas releases, public_html.devolvida-<hora>.
    if [ "$para" = volta-ao-antigo ]; then
        devolvida="$SITE/public_html.novo-$(date +%Y%m%d)"
    else
        devolvida="$SITE/public_html.devolvida-$(carimbo)"
    fi
    [ ! -e "$devolvida" ] || falhar "$devolvida já existe (uma volta de antes?): renomeie-a à mão, e rode de novo."
    confirmar "Voltar $DOMINIO para $anterior?"

    ONDE="voltar: as árvores"
    marcar_passo voltando
    mv -T "$ARVORE" "$devolvida"
    mv -T "$anterior" "$ARVORE"
    ONDE="voltar: as pastas"
    devolver_as_pastas "$devolvida" "$ARVORE"
    ONDE="voltar: a tela nova"
    if [ -d "$FRONT_RAIZ/novo.anterior" ]; then
        rm -rf "$FRONT_RAIZ/novo.devolvida"
        [ ! -d "$FRONT_RAIZ/novo" ] || mv -T "$FRONT_RAIZ/novo" "$FRONT_RAIZ/novo.devolvida"
        mv -T "$FRONT_RAIZ/novo.anterior" "$FRONT_RAIZ/novo"
        ok "a tela nova anterior de volta em $FRONT_RAIZ/novo"
    else
        aviso "sem $FRONT_RAIZ/novo.anterior: a tela nova desta release fica (o vhost de antes não a serve)."
    fi
    printf '%s voltou anterior=%s devolvida=%s\n' "$(carimbo)" "$anterior" "$devolvida" >> "$ESTADO/historico"
    rm -f "$ESTADO/anterior"
    if [ "$para" = volta-ao-antigo ]; then
        # O PHP-FPM DEPOIS DOS NOMES DAS PERMISSÕES, como o item 4 de "A volta":
        # recarregado antes, o SessaoAcl antigo lançava no login de todos (o
        # D701) até o devolver-nomes rodar. E o roteiro que os devolve é o da
        # árvore DEVOLVIDA -- a antiga, no lugar, não o tem.
        rm -f "$ESTADO/passo"
        ONDE=""
        cat <<FIM

De volta ao código antigo, em $(( $(date +%s) - inicio )) s. A árvore nova ficou em $devolvida (com os logs
que gravou). O PHP-FPM NÃO foi recarregado. Siga "A volta" do procedimento, do item 4:
    o Líder desfeito e o evento religado (como o item 4 diz), e os nomes das permissões de volta:
    mysql --default-character-set=utf8mb4 franquia_producao < $devolvida/scripts/devolver-nomes-das-permissoes-das-telas-novas.sql
    (sai com 0; com 1, PARE e leia), e SÓ ENTÃO: systemctl reload ${PHP_FPM_SERVICO:-<o serviço do php-fpm>}
    depois, o item 5 (conferir, e crontab $CRONTAB_GUARDADO) e o 6 (liberar).
FIM
        return 0
    fi
    ONDE="voltar: o PHP-FPM"
    recarregar_o_php_fpm
    rm -f "$ESTADO/passo"
    ONDE=""
    printf '\nDe volta: %s, em %s s. A release devolvida ficou em %s (com os logs que gravou). Falta, à mão: a conferência, o crontab desta noite (crontab %s) e liberar.\n' \
        "$(cut -c1-8 "$ARVORE/.publicar/release")" "$(( $(date +%s) - inicio ))" "$devolvida" "$CRONTAB_GUARDADO"
}

# As pastas de upload, o avatar e o app.json, de uma árvore para a outra (o
# trocar ao contrário). O que já existe no destino não é tocado: o mv -T para.
# ANTES DA TROCA (sem-app-json), o app.json da release ao lado é o do
# repositório, e não o de lá: copiá-lo apagaria o que a tela gravou.
devolver_as_pastas() {
    local de="$1" para="$2" app_json="${3:-com-app-json}" p
    for p in $PASTAS_DE_UPLOAD; do
        [ -e "$de/public/$p" ] || continue
        [ ! -e "$para/public/$p" ] || { aviso "$para/public/$p já existe: fica, e $de/public/$p também (olhe à mão)."; continue; }
        mv -T "$de/public/$p" "$para/public/$p"
    done
    if [ -d "$de/$AVATAR" ] && [ ! -e "$para/$AVATAR" ]; then
        mv -T "$de/$AVATAR" "$para/$AVATAR"
    fi
    # O app.json, se alguém o mudou no meio (a volta do procedimento, o 3).
    if [ "$app_json" = com-app-json ] && [ -f "$de/public/app.json" ] && ! cmp -s "$de/public/app.json" "$para/public/app.json"; then
        cp -a "$de/public/app.json" "$para/public/app.json"
        ok "o app.json mudado no meio veio junto"
    fi
    ok "pastas de upload e avatar de volta em $para"
}

cmd_conferir() {
    ser_root
    conferir_a_hora
    conferir_a_manutencao
    conferir_o_cron ida
    if [ -d "$NOVA" ]; then
        conferir_as_rotinas "$NOVA"
        conferir_a_impressao
    else
        ok "sem release ao lado: a impressão e as rotinas se conferem depois do levar"
    fi
    printf '\nTudo conferido.\n'
}

uso() {
    awk 'NR > 2 { if (/^# ----/) exit; sub(/^# ?/, ""); print }' "$0"
    exit "${1:-0}"
}

case "$SUBCOMANDO" in
    montar)   shift; cmd_montar "$@" ;;
    levar)    shift; cmd_levar "$@" ;;
    trocar)   shift; cmd_trocar "$@" ;;
    entregar) shift; cmd_entregar "$@" ;;
    voltar)   shift; cmd_voltar "$@" ;;
    conferir) shift; cmd_conferir "$@" ;;
    ''|-h|--help|ajuda) uso 0 ;;
    *) printf 'Subcomando desconhecido: %s\n\n' "$1" >&2; uso 1 ;;
esac
