#!/bin/bash # Emergency rollback for HM AI QC. # # Usage: # rollback.sh last Roll back to the checkpoint saved by deploy.sh # rollback.sh Roll back to an explicit commit # # Note: Alembic downgrade is intentionally NOT run here — schema rollbacks # are risky on data-bearing tables. If the bad deploy added a column the # rolled-back code doesn't know about, that's almost always fine. If it # dropped or renamed a column, restore from the daily DB backup. set -euo pipefail APP_DIR=${APP_DIR:-/opt/hm-aiqc} HEALTH_URL=${HEALTH_URL:-http://127.0.0.1:5050/health} ROLLBACK_FILE="$APP_DIR/.last_deploy_rollback" TARGET=${1:-} if [[ -z "$TARGET" || "$TARGET" == "last" ]]; then if [[ ! -f "$ROLLBACK_FILE" ]]; then echo "No .last_deploy_rollback file. Specify a commit hash explicitly." echo "Usage: $(basename "$0") last | " exit 1 fi TARGET=$(cat "$ROLLBACK_FILE") fi cd "$APP_DIR" if ! git rev-parse --verify --quiet "$TARGET^{commit}" > /dev/null; then echo "ERROR: Commit '$TARGET' not found" exit 1 fi CURRENT_REV=$(git rev-parse HEAD) CURRENT_SHORT=$(git rev-parse --short HEAD) TARGET_REV=$(git rev-parse "$TARGET") TARGET_SHORT=$(git rev-parse --short "$TARGET") if [[ "$CURRENT_REV" == "$TARGET_REV" ]]; then echo "Already at $TARGET_SHORT — nothing to do." exit 0 fi echo "============================================" echo " HM AI QC rollback" echo "============================================" echo "Current: $CURRENT_SHORT $(git log -1 --format='%s' HEAD)" echo "Target: $TARGET_SHORT $(git log -1 --format='%s' "$TARGET")" echo "" read -r -p "Proceed? (y/N): " confirm if [[ ! $confirm =~ ^[Yy]$ ]]; then echo "Cancelled." exit 0 fi git reset --hard "$TARGET_REV" docker compose build docker compose up -d # 60s window — same as deploy.sh for i in {1..30}; do sleep 2 if curl -sf -o /dev/null "$HEALTH_URL"; then echo "Rollback OK. Now at $TARGET_SHORT." exit 0 fi done echo "Service unhealthy after rollback." echo "docker compose logs --tail=200 web" exit 1