#!/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- 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"