ec805957f1
Портал выкатывается полной заменой public/build; сборка из устаревшей ветки молча сносила чужую работу (инцидент 08.07: Метрика снесла колонки «Сделок»). deploy-build.sh перед подменой сверяет маркеры критичных фич (по активным чанкам manifest.json, не по orphan-осколкам) — теряет живую фичу → ОТКАЗ. Осознанный откат — FORCE=1, только проверка — DRY_RUN=1. Установлен на боевом. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
7.0 KiB
Bash
115 lines
7.0 KiB
Bash
#!/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" # автоподбор конкурентов
|
||
)
|
||
|
||
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"
|