Files
portal/deploy/deploy-build.sh
T
Дмитрий ec805957f1 chore(deploy): защищённый выкат сборки портала — не даёт затереть чужие фичи
Портал выкатывается полной заменой 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>
2026-07-08 19:19:17 +03:00

115 lines
7.0 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"