Files
portal/deploy/deploy-build.sh
T

116 lines
7.1 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Лидерра — ЗАЩИЩЁННЫЙ выкат фронт-сборки портала (public/build).
#
# ЗАЧЕМ. Портал выкатывается ПОЛНОЙ заменой папки public/build. Если собрать
# из УСТАРЕВШЕЙ ветки (где нет чужих свежих фич) и залить — чужая работа
# МОЛЧА затирается. Реальный инцидент 08.07.2026: выкат Яндекс.Метрики,
# собранный из ветки без колонок «Сделок», снёс колонки «Конкурент/Источник».
#
# ЧТО ДЕЛАЕТ. Перед подменой сверяет: новая сборка НЕ должна ПОТЕРЯТЬ ни одной
# критичной фичи, которая есть в живой сборке. Потеряла — ОТКАЗ, боевой не тронут.
# Осознанный откат (когда фичу убираем намеренно) — только с FORCE=1.
#
# ВАЖНО. Маркеры ищем ТОЛЬКО в чанках, на которые ссылается manifest.json
# (активная сборка), а не по всей assets/ — там копятся «осколки» старых
# overlay-выкатов, из-за которых наивная проверка промахивалась.
#
# ИСПОЛЬЗОВАНИЕ (с dev-машины):
# tar czf build.tgz -C app/public/build .
# scp build.tgz liderra:/tmp/build.tgz
# ssh liderra 'sudo bash /var/www/liderra/deploy-build.sh /tmp/build.tgz "ветка/sha/что выкатываем"'
# # только проверить, не выкатывая:
# ssh liderra 'sudo DRY_RUN=1 bash /var/www/liderra/deploy-build.sh /tmp/build.tgz "проверка"'
# # осознанный откат/удаление фичи:
# ssh liderra 'sudo FORCE=1 bash /var/www/liderra/deploy-build.sh /tmp/build.tgz "rollback: ..."'
#
# Откат в любой момент: mv build.bak-guard-<ts> build (см. вывод скрипта).
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
TGZ="${1:?укажи путь к tar.gz со сборкой (содержимое папки build)}"
INFO="${2:-}"
FORCE="${FORCE:-}"
DRY_RUN="${DRY_RUN:-}"
BASE=/var/www/liderra/app/public
LIVE="$BASE/build"
# Критичные маркеры фич: если строка есть в ЖИВОЙ сборке — обязана быть и в новой.
# ➕ Добавляй строку сюда, когда выкатываешь новую пользовательскую фичу,
# чтобы её тоже нельзя было случайно затереть следующим выкатом.
MARKERS=(
"Конкурент" # колонки «Сделок»: Конкурент / Источник
"mc.yandex" # Яндекс.Метрика (счётчик + Вебвизор)
"ym-hide-content" # маскировка ПДн лидов для Метрики (152-ФЗ) — критично
"deal-card" # мобильные/планшетные карточки сделок
"autopodbor" # автоподбор конкурентов
"warming/yandex" # прогрев кандидатов: три площадки (Яндекс/ВК/Телеграм) — экран SalesWarmingView
)
echo "▶ Защищённый выкат сборки портала. Инфо: ${INFO:-}"
[ -f "$TGZ" ] || { echo "❌ Нет файла: $TGZ"; exit 2; }
STAGING="$BASE/.build-staging.$$"
rm -rf "$STAGING"; mkdir -p "$STAGING"
tar xzf "$TGZ" -C "$STAGING"
[ -f "$STAGING/manifest.json" ] || { echo "❌ В сборке нет manifest.json — это не билд портала."; rm -rf "$STAGING"; exit 2; }
# Список АКТИВНЫХ чанков (js/css) по manifest — без orphan-осколков прошлых выкатов.
active_files() { # $1 = dir сборки → печатает полные пути к активным чанкам
local dir="$1"
grep -oE 'assets/[A-Za-z0-9_.-]+\.(js|css)' "$dir/manifest.json" | sort -u | sed "s|^|$dir/|"
}
mapfile -t LIVE_FILES < <(active_files "$LIVE" 2>/dev/null || true)
mapfile -t NEW_FILES < <(active_files "$STAGING" 2>/dev/null || true)
hits() { # $1 = маркер, далее список файлов → сколько файлов содержат маркер
local m="$1"; shift
[ "$#" -eq 0 ] && { echo 0; return 0; }
# grep возвращает 1 при «не найдено»; при pipefail+set -e это рвёт скрипт —
# поэтому глушим код через "|| true", а число берём из вывода wc.
local c
c=$(grep -lF -- "$m" "$@" 2>/dev/null | wc -l) || true
echo "${c:-0}"
}
# ── ГЛАВНАЯ ЗАЩИТА: новая сборка не должна терять фичи живой ──────────────────
missing=()
for m in "${MARKERS[@]}"; do
live_n=$(hits "$m" "${LIVE_FILES[@]}")
new_n=$(hits "$m" "${NEW_FILES[@]}")
if [ "$live_n" -gt 0 ] && [ "$new_n" -eq 0 ]; then
missing+=("$m")
fi
done
if [ "${#missing[@]}" -gt 0 ] && [ "$FORCE" != "1" ]; then
echo ""
echo "🛑 ОТКАЗ: новая сборка ТЕРЯЕТ фичи, которые есть на боевом:"
printf ' • %s\n' "${missing[@]}"
echo ""
echo " Похоже, собрано из УСТАРЕВШЕЙ ветки (без чужой работы)."
echo " Пересобери из ветки-надмножества — где есть ВСЁ живое."
echo " Если это ОСОЗНАННЫЙ откат/удаление фичи — повтори с FORCE=1."
rm -rf "$STAGING"
exit 3
fi
if [ "${#missing[@]}" -gt 0 ]; then
echo "⚠ FORCE=1: осознанно теряем фичи (${missing[*]}) — по прямому указанию."
fi
if [ "$DRY_RUN" = "1" ]; then
echo "✅ DRY_RUN: проверка пройдена, все живые фичи на месте. Выкат НЕ выполнялся."
rm -rf "$STAGING"
exit 0
fi
# ── Подмена с бэкапом и отметкой о выкате ────────────────────────────────────
ts="$(date -u +%Y%m%d-%H%M%S)"
mv "$LIVE" "$BASE/build.bak-guard-$ts"
mv "$STAGING" "$LIVE"
appjs=$(grep -oE 'assets/app-[a-zA-Z0-9_-]+\.js' "$LIVE/manifest.json" | head -1)
printf '{"deployed_at":"%s","info":"%s","bundle":"%s","guard_ok":true}\n' \
"$ts" "${INFO//\"/}" "$appjs" > "$LIVE/DEPLOYED.json"
chown -R www-data:www-data "$LIVE"
echo "✅ Выкат ок. Бандл: $appjs. Бэкап прежней сборки: build.bak-guard-$ts"