#!/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 [--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 [--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