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>
113 lines
4.1 KiB
Bash
Executable file
113 lines
4.1 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# Sprint 21: быстрый rollback на предыдущий tag.
|
||
#
|
||
# Алгоритм:
|
||
# 1. Проверить что image нужного tag'a есть в registry (docker pull)
|
||
# 2. Перезапустить api/web с этим tag'ом через docker compose
|
||
# (через ENV API_TAG/WEB_TAG → compose pick'ает)
|
||
# 3. Дождаться /health/ready на новом контейнере
|
||
# 4. Если health OK → выйти 0; если не OK → fail (но контейнер уже
|
||
# поднят, ручное вмешательство нужно)
|
||
#
|
||
# Миграции БД rollback скрипт НЕ откатывает: down-migrations EF Core
|
||
# поддерживает, но мы их не пишем (см. CLAUDE.md / Phase19a/b — обе
|
||
# имеют Down() для DROP'a, но это для прода опасно — данные потеряются).
|
||
# Если откат требует down-миграции — отдельный manual review.
|
||
#
|
||
# Usage:
|
||
# deploy/prod-rollback.sh <to-tag> [--dry-run] [--skip-web]
|
||
#
|
||
# Example:
|
||
# deploy/prod-rollback.sh v20260606.5
|
||
# deploy/prod-rollback.sh v20260606.5 --dry-run
|
||
|
||
set -uo pipefail
|
||
|
||
TO_TAG="${1:-}"
|
||
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 ;;
|
||
--help|-h) grep -E '^#( |$)' "$0" | sed 's/^# \?//'; exit 0 ;;
|
||
-*) echo "Unknown: $1" >&2; exit 2 ;;
|
||
*) shift ;;
|
||
esac
|
||
done
|
||
|
||
if [[ -z "$TO_TAG" ]]; then
|
||
echo "Usage: $0 <to-tag> [--dry-run] [--skip-web]" >&2
|
||
exit 2
|
||
fi
|
||
|
||
REGISTRY="${REGISTRY:-127.0.0.1:5001}"
|
||
COMPOSE_PATH="${COMPOSE_PATH:-/home/nns/food-market-prod/deploy/docker-compose.yml}"
|
||
PROD_URL="${PROD_URL:-https://admin.food-market.kz}"
|
||
|
||
log() { echo "[$(date -Iseconds)] $*"; }
|
||
fail() { log "FAIL: $*"; exit 1; }
|
||
|
||
run() {
|
||
if [[ $DRY_RUN -eq 1 ]]; then echo "[dry-run] $*";
|
||
else echo "[exec] $*"; "$@"; fi
|
||
}
|
||
|
||
# ── 1. Validate image existence ──────────────────────────────────────
|
||
log "=== Step 1/3: validate images ==="
|
||
API_IMG="$REGISTRY/food-market-api:$TO_TAG"
|
||
WEB_IMG="$REGISTRY/food-market-web:$TO_TAG"
|
||
|
||
# Сначала пробуем docker image inspect — если уже скачан, не тянем.
|
||
if [[ $DRY_RUN -eq 0 ]]; then
|
||
if ! docker image inspect "$API_IMG" >/dev/null 2>&1; then
|
||
log "api image $API_IMG не скачан, pull'им"
|
||
docker pull "$API_IMG" || fail "api image $TO_TAG отсутствует в $REGISTRY"
|
||
else
|
||
log "api image $TO_TAG уже скачан"
|
||
fi
|
||
if [[ $SKIP_WEB -eq 0 ]]; then
|
||
if ! docker image inspect "$WEB_IMG" >/dev/null 2>&1; then
|
||
docker pull "$WEB_IMG" || fail "web image $TO_TAG отсутствует"
|
||
fi
|
||
fi
|
||
else
|
||
log "[dry-run] would pull $API_IMG (and $WEB_IMG)"
|
||
fi
|
||
|
||
# ── 2. docker compose up -d с новым tag ─────────────────────────────
|
||
log "=== Step 2/3: docker compose up -d ==="
|
||
if [[ ! -f "$COMPOSE_PATH" ]]; then
|
||
fail "compose не найден: $COMPOSE_PATH"
|
||
fi
|
||
cd "$(dirname "$COMPOSE_PATH")"
|
||
|
||
if [[ $DRY_RUN -eq 0 ]]; then
|
||
if [[ $SKIP_WEB -eq 1 ]]; then
|
||
API_TAG="$TO_TAG" docker compose up -d --force-recreate api
|
||
else
|
||
API_TAG="$TO_TAG" WEB_TAG="$TO_TAG" docker compose up -d --force-recreate api web
|
||
fi
|
||
else
|
||
log "[dry-run] would run: API_TAG=$TO_TAG WEB_TAG=$TO_TAG docker compose up -d --force-recreate api web"
|
||
fi
|
||
|
||
# ── 3. Wait /health/ready ────────────────────────────────────────────
|
||
log "=== Step 3/3: wait /health/ready ==="
|
||
if [[ $DRY_RUN -eq 0 ]]; then
|
||
for i in $(seq 1 60); do
|
||
sleep 2
|
||
if curl -fsS --max-time 5 "$PROD_URL/health/ready" 2>/dev/null | grep -q '"status":"Healthy"'; then
|
||
log "✓ Rollback complete: $PROD_URL Healthy после $((i*2))s"
|
||
exit 0
|
||
fi
|
||
done
|
||
fail "/health/ready не отвечает Healthy за 120с — ручное вмешательство"
|
||
else
|
||
log "[dry-run] would poll $PROD_URL/health/ready up to 60×2s"
|
||
fi
|
||
|
||
exit 0
|