food-market/deploy/check-prod-readiness.sh
nns 843fc4bd03
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
feat(s21): stage→prod migration toolchain (7 скриптов + workflow)
1. deploy/check-prod-readiness.sh — pre-deploy gating: backup<60min,
   disk≥5GB на /opt+/var/lib/docker, /health/ready=Healthy, .env
   required-keys без placeholder'ов. --ssh-host для удалённой проверки.

2. deploy/prod-deploy.sh <api-tag> <web-tag> — blue-green release:
   pull → green-контейнер на :8088 → migrations (auto) → smoke
   (/health/ready + /api/me с тест-токеном) → nginx upstream switch
   → swap → docker compose up -d с обновлённым тэгом. Failure →
   удаление green, blue остаётся. --skip-web флаг.

3. deploy/prod-rollback.sh <to-tag> — docker pull (если нужно) →
   docker compose up -d --force-recreate с указанным tag'ом → wait
   /health/ready до 120с. --dry-run + --skip-web.

4. deploy/post-deploy-smoke.sh — 10 шагов (signup → login →
   /api/me → list products/counterparties/stores/stock → create+delete
   product → logout-via-session). JSON парсится через python3
   (не grep — споткнулись на пробеле перед `:` в access_token).
   Telegram-alert через FM_TG_TOKEN/CHAT при провале. Stage-тест: 10/10 ✓.

5. deploy/db-schema-diff.sh — pg_dump --schema-only с обоих хостов
   через ssh+docker exec, нормализация (sed), diff -u. Exit:
   0=идентичны, 1=разница, 2=ошибка.

6. deploy/generate-release-notes.sh <from-tag> <to-tag> — git log
   group by prefix через awk: feat→, fix→🐛, perf→, docs→📚,
   test/refactor/chore→<details>. Сохраняет docs/release-notes/<tag>.md.

7. .forgejo/workflows/auto-tag.yml — на push в main: если HEAD не
   помечен → создаёт v<YYYYMMDD>.<N> annotated tag, push в origin,
   генерирует release-notes для будущего деплоя.

Все скрипты идемпотентные, поддерживают --dry-run, не трогают прод.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:31:10 +05:00

172 lines
6.8 KiB
Bash
Executable file
Raw Permalink 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
#
# Sprint 21: pre-deploy readiness check на prod-vm.
#
# Запускается ПЕРЕД prod-deploy.sh. Проверяет:
# 1. Backup сделан < FM_BACKUP_MAX_AGE_MIN (60) минут назад
# 2. Свободного места ≥ FM_MIN_FREE_GB (5) GB на каждом mount
# 3. Текущий /health/ready возвращает 200
# 4. (опц.) CI-проверки на этом коммите прошли (FM_CHECK_CI=1)
# 5. .env содержит все required переменные (без placeholder'ов
# типа CHANGEME/REPLACE_ME)
#
# Exit 0 — всё хорошо, можно деплоить.
# Exit 1+ — конкретная причина в stderr.
#
# Usage:
# deploy/check-prod-readiness.sh [--dry-run] [--ssh-host HOST]
#
# По умолчанию проверки локальные (предполагается что скрипт запущен НА
# prod-vm). С --ssh-host выполняется через ssh: запускает себя
# удалённо.
set -uo pipefail
# -e снят: хотим прогнать ВСЕ проверки и собрать суммарный отчёт,
# не вылетая на первой.
DRY_RUN=0
SSH_HOST=""
PROD_URL="${PROD_URL:-https://admin.food-market.kz}"
BACKUP_DIR="${FM_BACKUP_DIR:-/opt/food-market-data/backups}"
ENV_FILE="${FM_ENV_FILE:-/home/nns/food-market-prod/deploy/.env}"
MAX_AGE_MIN="${FM_BACKUP_MAX_AGE_MIN:-60}"
MIN_FREE_GB="${FM_MIN_FREE_GB:-5}"
MOUNTS="${FM_MOUNTS:-/opt /var/lib/docker}"
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--ssh-host) SSH_HOST="$2"; shift 2 ;;
--help|-h)
grep -E '^#( |$)' "$0" | sed 's/^# \?//'; exit 0 ;;
*) echo "Unknown arg: $1" >&2; exit 2 ;;
esac
done
if [[ -n "$SSH_HOST" ]]; then
# Re-run self on remote host.
echo "[check] proxying to $SSH_HOST"
exec ssh "$SSH_HOST" "FM_BACKUP_DIR='$BACKUP_DIR' FM_ENV_FILE='$ENV_FILE' \
FM_BACKUP_MAX_AGE_MIN='$MAX_AGE_MIN' FM_MIN_FREE_GB='$MIN_FREE_GB' \
FM_MOUNTS='$MOUNTS' PROD_URL='$PROD_URL' bash -s" < "$0" $([[ $DRY_RUN -eq 1 ]] && echo --dry-run)
fi
PASS=0
FAIL=0
ERRORS=()
check() {
local name="$1" status="$2" detail="$3"
if [[ "$status" == "OK" ]]; then
echo "[✓] $name$detail"
((PASS+=1))
else
echo "[✗] $name$detail" >&2
ERRORS+=("$name: $detail")
((FAIL+=1))
fi
}
# ── 1. Backup age ────────────────────────────────────────────────────
if [[ ! -d "$BACKUP_DIR" ]]; then
check "backup-age" "FAIL" "backup-dir $BACKUP_DIR не существует"
else
# Самый свежий db-*.dump
LATEST=$(ls -t "$BACKUP_DIR"/db-*.dump 2>/dev/null | head -1)
if [[ -z "$LATEST" ]]; then
check "backup-age" "FAIL" "в $BACKUP_DIR нет db-*.dump файлов"
else
AGE_SEC=$(( $(date +%s) - $(stat -c %Y "$LATEST") ))
AGE_MIN=$(( AGE_SEC / 60 ))
if (( AGE_MIN > MAX_AGE_MIN )); then
check "backup-age" "FAIL" "последний backup $LATEST: $AGE_MIN мин назад (порог $MAX_AGE_MIN)"
else
check "backup-age" "OK" "$AGE_MIN мин назад ($LATEST)"
fi
fi
fi
# ── 2. Free disk space ───────────────────────────────────────────────
for mnt in $MOUNTS; do
if [[ ! -d "$mnt" ]]; then
check "disk:$mnt" "FAIL" "mount не существует"
continue
fi
FREE_KB=$(df --output=avail "$mnt" 2>/dev/null | tail -1 | tr -d ' ')
if [[ -z "$FREE_KB" || "$FREE_KB" -le 0 ]]; then
check "disk:$mnt" "FAIL" "df не вернул avail"
continue
fi
FREE_GB=$(( FREE_KB / 1024 / 1024 ))
if (( FREE_GB < MIN_FREE_GB )); then
check "disk:$mnt" "FAIL" "$FREE_GB GB свободно (порог $MIN_FREE_GB)"
else
check "disk:$mnt" "OK" "$FREE_GB GB свободно"
fi
done
# ── 3. /health/ready ─────────────────────────────────────────────────
HEALTH_BODY=$(curl -fsS --max-time 10 "$PROD_URL/health/ready" 2>/dev/null || echo "")
if echo "$HEALTH_BODY" | grep -q '"status":"Healthy"'; then
check "health-ready" "OK" "$PROD_URL/health/ready=Healthy"
else
check "health-ready" "FAIL" "ответ: ${HEALTH_BODY:-<empty>}"
fi
# ── 4. CI status (опц.) ──────────────────────────────────────────────
if [[ "${FM_CHECK_CI:-0}" == "1" ]]; then
# Берём текущий commit и проверяем что CI workflow прошёл.
# Реализация зависит от наличия Forgejo CLI; пока — manual hint.
CURRENT_SHA=$(cd /home/nns/food-market 2>/dev/null && git rev-parse HEAD 2>/dev/null || echo "")
if [[ -n "$CURRENT_SHA" ]]; then
check "ci-status" "OK" "skipped (manual check: GET /api/v1/repos/nns/food-market/commits/$CURRENT_SHA/status)"
else
check "ci-status" "OK" "skipped (not a git repo)"
fi
fi
# ── 5. .env complete ─────────────────────────────────────────────────
if [[ ! -f "$ENV_FILE" ]]; then
check ".env-file" "FAIL" "$ENV_FILE не существует"
else
# Список обязательных переменных для прод-окружения.
REQUIRED=(
"POSTGRES_PASSWORD"
"OPENIDDICT_ISSUER"
"OPENIDDICT_CERT_PASSWORD"
)
MISSING=()
PLACEHOLDER=()
for key in "${REQUIRED[@]}"; do
VAL=$(grep -E "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2- || true)
if [[ -z "$VAL" ]]; then
MISSING+=("$key")
elif [[ "$VAL" =~ ^(CHANGEME|REPLACE_ME|TODO|dev|food_market_dev)$ ]]; then
PLACEHOLDER+=("$key=$VAL")
fi
done
if (( ${#MISSING[@]} > 0 )); then
check ".env-required" "FAIL" "отсутствуют: ${MISSING[*]}"
fi
if (( ${#PLACEHOLDER[@]} > 0 )); then
check ".env-placeholders" "FAIL" "плейсхолдеры: ${PLACEHOLDER[*]}"
fi
if (( ${#MISSING[@]} == 0 && ${#PLACEHOLDER[@]} == 0 )); then
check ".env-file" "OK" "${#REQUIRED[@]} required key(s) заполнены"
fi
fi
# ── Итог ─────────────────────────────────────────────────────────────
echo
echo "==> $PASS passed, $FAIL failed"
if (( FAIL > 0 )); then
echo "Не готов к деплою:"
for e in "${ERRORS[@]}"; do echo " - $e"; done
exit 1
fi
if [[ $DRY_RUN -eq 1 ]]; then
echo "(dry-run; никаких изменений)"
fi
echo "OK — можно запускать prod-deploy.sh"
exit 0