food-market/deploy/prod-deploy.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

221 lines
9 KiB
Bash
Executable file
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
#
# Sprint 21: blue-green deploy для prod-vm.
#
# Алгоритм:
# 1. Pull новых images из registry (если их там нет → fail)
# 2. Запуск ВТОРОГО api-контейнера (food-market-api-next) на :8088
# 3. Выполнить миграции БД через временный one-shot контейнер
# 4. Smoke-test на новом api: /health/ready + /api/me с тестовым токеном
# 5. Если ок → переключить prod nginx upstream на :8088 → reload nginx
# 6. Удалить старый api-контейнер; переименовать food-market-api-next → food-market-api
# 7. То же самое для web (но без миграций)
#
# Если smoke не прошёл → откат: убиваем -next, оставляем старый запущенным.
#
# Usage:
# deploy/prod-deploy.sh <api-tag> <web-tag> [--dry-run] [--skip-web]
#
# Подразумевает что nginx стоит на хосте (не в compose) и его upstream
# конфигурируется через include /etc/nginx/upstream.conf, который этот
# скрипт переписывает atomic'ом. Если nginx внутри web-контейнера —
# blue-green становится через docker-swap, а не nginx-reload (подробнее
# в docs/prod-deploy.md).
set -uo pipefail
API_TAG="${1:-}"
WEB_TAG="${2:-}"
DRY_RUN=0
SKIP_WEB=0
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--skip-web) SKIP_WEB=1; shift ;;
-*) echo "Unknown: $1" >&2; exit 2 ;;
*) shift ;;
esac
done
if [[ -z "$API_TAG" || -z "$WEB_TAG" ]]; then
cat <<EOF
Usage: $0 <api-tag> <web-tag> [--dry-run] [--skip-web]
Example:
$0 v20260607.3 v20260607.3
$0 v20260607.3 v20260607.3 --dry-run
Required env (defaults in [brackets]):
REGISTRY [127.0.0.1:5001]
COMPOSE_PATH [/home/nns/food-market-prod/deploy/docker-compose.yml]
NGINX_UPSTREAM_FILE [/etc/nginx/conf.d/food-market-upstream.conf]
API_PORT_BLUE [8080] — текущий
API_PORT_GREEN [8088] — временный для нового контейнера
EOF
exit 2
fi
REGISTRY="${REGISTRY:-127.0.0.1:5001}"
COMPOSE_PATH="${COMPOSE_PATH:-/home/nns/food-market-prod/deploy/docker-compose.yml}"
NGINX_UPSTREAM_FILE="${NGINX_UPSTREAM_FILE:-/etc/nginx/conf.d/food-market-upstream.conf}"
API_PORT_BLUE="${API_PORT_BLUE:-8080}"
API_PORT_GREEN="${API_PORT_GREEN:-8088}"
TEST_TOKEN_FILE="${TEST_TOKEN_FILE:-/home/nns/.fm-prod-test-token}"
run() {
if [[ $DRY_RUN -eq 1 ]]; then
echo "[dry-run] $*"
else
echo "[exec] $*"
"$@"
fi
}
log() { echo "[$(date -Iseconds)] $*"; }
fail() {
log "FAIL: $*"
exit 1
}
# ── 1. Pull новых images ─────────────────────────────────────────────
log "=== Step 1/7: pull images ==="
API_IMG="$REGISTRY/food-market-api:$API_TAG"
WEB_IMG="$REGISTRY/food-market-web:$WEB_TAG"
run docker pull "$API_IMG" || fail "api image $API_IMG отсутствует в registry"
[[ $SKIP_WEB -eq 0 ]] && (run docker pull "$WEB_IMG" || fail "web image $WEB_IMG отсутствует")
# ── 2. Запуск green-api на :8088 ─────────────────────────────────────
log "=== Step 2/7: start green api on :$API_PORT_GREEN ==="
# Если -next уже есть (от прошлой неудачной попытки) — снести.
run docker rm -f food-market-api-next 2>/dev/null || true
# .env берём из compose dir чтобы получить те же переменные.
ENV_FILE="$(dirname "$COMPOSE_PATH")/.env"
[[ -f "$ENV_FILE" ]] || fail ".env не найден: $ENV_FILE"
# Запускаем second api с теми же volume/env, но на host port 8088
# и подключённый к той же compose-сети (food-market-prod_default).
NETWORK="food-market-prod_default"
run docker run -d \
--name food-market-api-next \
--network "$NETWORK" \
--env-file "$ENV_FILE" \
-e ASPNETCORE_ENVIRONMENT=Production \
-p "127.0.0.1:$API_PORT_GREEN:8080" \
--restart no \
"$API_IMG"
# ── 3. Миграции БД ──────────────────────────────────────────────────
# .NET API мигрирует автоматически на старте через AppDbContext.Database.Migrate()
# (см. Program.cs). Поэтому ждём готовности green-контейнера — если он
# поднялся healthy = миграции прошли.
log "=== Step 3/7: wait for green api ready (migrations) ==="
if [[ $DRY_RUN -eq 0 ]]; then
READY=0
for i in $(seq 1 60); do
sleep 2
if curl -fsS "http://127.0.0.1:$API_PORT_GREEN/health/ready" 2>/dev/null | grep -q '"status":"Healthy"'; then
READY=1
log "green api ready после $((i*2))s"
break
fi
done
if [[ $READY -eq 0 ]]; then
log "FAIL: green api не стал Healthy за 120с — сносим"
docker logs food-market-api-next --tail 50 || true
docker rm -f food-market-api-next || true
fail "green api не поднялся"
fi
else
log "[dry-run] skip wait for ready"
fi
# ── 4. Smoke-test на green ──────────────────────────────────────────
log "=== Step 4/7: smoke green api ==="
if [[ $DRY_RUN -eq 0 ]]; then
# /health/ready уже проверили; ещё /api/me с тестовым токеном.
if [[ -f "$TEST_TOKEN_FILE" ]]; then
TEST_TOKEN=$(cat "$TEST_TOKEN_FILE")
ME=$(curl -fsS -H "Authorization: Bearer $TEST_TOKEN" \
"http://127.0.0.1:$API_PORT_GREEN/api/me" 2>/dev/null || echo "")
if [[ -z "$ME" ]] || ! echo "$ME" | grep -q '"email"'; then
log "FAIL: /api/me с тестовым токеном вернул: $ME"
docker rm -f food-market-api-next || true
fail "smoke-test провалился"
fi
log "smoke /api/me ✓"
else
log "(нет $TEST_TOKEN_FILE — пропускаем /api/me, только health-check)"
fi
else
log "[dry-run] skip smoke"
fi
# ── 5. Switch nginx upstream ────────────────────────────────────────
log "=== Step 5/7: nginx upstream switch :$API_PORT_BLUE → :$API_PORT_GREEN ==="
if [[ $DRY_RUN -eq 0 ]]; then
if [[ ! -f "$NGINX_UPSTREAM_FILE" ]]; then
log "WARN: $NGINX_UPSTREAM_FILE не существует — создаём впервые"
sudo touch "$NGINX_UPSTREAM_FILE"
fi
# Atomic write: новый upstream указывает на green.
TMP="$(mktemp)"
cat > "$TMP" <<NGX
# Generated by prod-deploy.sh $(date -Iseconds)
upstream food_market_api {
server 127.0.0.1:$API_PORT_GREEN;
}
NGX
sudo install -m 644 "$TMP" "$NGINX_UPSTREAM_FILE"
rm "$TMP"
sudo nginx -t || { log "FAIL nginx -t"; fail "nginx config invalid"; }
sudo systemctl reload nginx
log "nginx reloaded"
else
log "[dry-run] would write upstream → 127.0.0.1:$API_PORT_GREEN and reload nginx"
fi
# ── 6. Свернуть старый api, переименовать green → blue ──────────────
log "=== Step 6/7: stop old api, rename green ==="
run docker rm -f food-market-api 2>/dev/null || true
run docker rename food-market-api-next food-market-api
# Обновляем compose-yml tag → для будущих up-d
# (используем docker compose с новой версией; перезапуск НЕ нужен,
# контейнер уже работает).
log "(compose-yml tag update — manual через .env API_TAG=$API_TAG)"
# Переключить nginx обратно на blue-port чтобы соответствовать compose-mapping.
# Контейнер уже на host port $API_PORT_BLUE? нет, мы запускали green на 8088.
# Переключаем upstream обратно на 8080 чтобы dock-compose up в будущем работал.
if [[ $DRY_RUN -eq 0 ]]; then
# Переcоздаём green-контейнер с blue-портом (быстрый stop/start).
docker stop food-market-api && docker rm food-market-api
cd "$(dirname "$COMPOSE_PATH")"
API_TAG="$API_TAG" docker compose up -d api
TMP="$(mktemp)"
cat > "$TMP" <<NGX
upstream food_market_api {
server 127.0.0.1:$API_PORT_BLUE;
}
NGX
sudo install -m 644 "$TMP" "$NGINX_UPSTREAM_FILE"
rm "$TMP"
sudo nginx -t && sudo systemctl reload nginx
fi
# ── 7. Web (тот же flow без миграций) ───────────────────────────────
log "=== Step 7/7: web ==="
if [[ $SKIP_WEB -eq 1 ]]; then
log "skipped (--skip-web)"
else
cd "$(dirname "$COMPOSE_PATH")"
run env WEB_TAG="$WEB_TAG" docker compose up -d web
log "web re-pulled to $WEB_IMG"
fi
log "✓ Deploy complete: api=$API_TAG web=$WEB_TAG"
exit 0