Files
sori.studio/scripts/migrate-production-db.sh
zenn cc34db40f2 v1.3.7: NAS용 마이그레이션 셸 명령 추가
운영 호스트에 npm이 없어도 Docker Compose와 DB 컨테이너 psql만으로 상태 확인, baseline, 미적용 SQL 실행을 처리한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 14:39:44 +09:00

172 lines
4.5 KiB
Bash

#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
ENV_FILE=${ENV_FILE:-.env.production}
DB_SERVICE=${DB_SERVICE:-sori-studio-db}
MIGRATIONS_DIR="$ROOT_DIR/db/migrations"
CONTAINER_MIGRATIONS_DIR=${CONTAINER_MIGRATIONS_DIR:-/docker-entrypoint-initdb.d}
SCHEMA_MIGRATIONS_TABLE=${SCHEMA_MIGRATIONS_TABLE:-schema_migrations}
MODE=${1:-migrate}
BASELINE_TARGET=${2:-}
cd "$ROOT_DIR"
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
fi
POSTGRES_DB=${POSTGRES_DB:-sori_studio}
POSTGRES_USER=${POSTGRES_USER:-sori_studio}
compose() {
docker compose --env-file "$ENV_FILE" "$@"
}
psql_exec() {
compose exec -T "$DB_SERVICE" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@"
}
query_sql() {
psql_exec -At -c "$1" | tr -d '\r'
}
run_sql() {
psql_exec -c "$1"
}
ensure_database_ready() {
compose up -d "$DB_SERVICE"
attempt=1
while [ "$attempt" -le 20 ]; do
if compose exec -T "$DB_SERVICE" pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
echo "DB가 준비되지 않았습니다." >&2
exit 1
}
has_schema_migrations_table() {
[ "$(query_sql "SELECT to_regclass('public.$SCHEMA_MIGRATIONS_TABLE') IS NOT NULL;")" = "t" ]
}
has_application_schema() {
[ "$(query_sql "SELECT to_regclass('public.posts') IS NOT NULL;")" = "t" ]
}
ensure_schema_migrations_table() {
run_sql "CREATE TABLE IF NOT EXISTS $SCHEMA_MIGRATIONS_TABLE (
file_name TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);"
}
migration_files() {
find "$MIGRATIONS_DIR" -maxdepth 1 -type f -name '*.sql' -exec basename {} \; | sort
}
is_applied() {
file_name=$1
[ "$(query_sql "SELECT EXISTS (SELECT 1 FROM $SCHEMA_MIGRATIONS_TABLE WHERE file_name = '$file_name');")" = "t" ]
}
print_status() {
if ! has_schema_migrations_table; then
echo "schema_migrations 테이블이 없습니다."
echo "기존 운영 DB라면 먼저 sh scripts/migrate-production-db.sh baseline 으로 현재 파일들을 적용 완료로 기록하세요."
migration_files | while IFS= read -r file_name; do
echo "pending $file_name"
done
return 0
fi
migration_files | while IFS= read -r file_name; do
if is_applied "$file_name"; then
echo "applied $file_name"
else
echo "pending $file_name"
fi
done
}
baseline_migrations() {
ensure_schema_migrations_table
baseline_count=0
found_target=0
migration_files | while IFS= read -r file_name; do
if [ -n "$BASELINE_TARGET" ] && [ "$found_target" -eq 1 ]; then
continue
fi
run_sql "INSERT INTO $SCHEMA_MIGRATIONS_TABLE (file_name) VALUES ('$file_name') ON CONFLICT (file_name) DO NOTHING;"
echo "baseline $file_name"
baseline_count=$((baseline_count + 1))
if [ -n "$BASELINE_TARGET" ] && { [ "$file_name" = "$BASELINE_TARGET" ] || echo "$file_name" | grep -q "^${BASELINE_TARGET}_"; }; then
found_target=1
fi
done
if [ -n "$BASELINE_TARGET" ] && ! migration_files | grep -q "^${BASELINE_TARGET}_\\|^${BASELINE_TARGET}$"; then
echo "기준 마이그레이션을 찾을 수 없습니다: $BASELINE_TARGET" >&2
exit 1
fi
echo "baseline 완료"
}
migrate_database() {
if ! has_schema_migrations_table && has_application_schema; then
echo "운영 DB에 기존 스키마가 있지만 schema_migrations 적용 이력이 없습니다." >&2
echo "데이터 보호를 위해 001부터 자동 실행하지 않습니다." >&2
echo "현재 운영 DB가 최신 상태라면 sh scripts/migrate-production-db.sh baseline 으로 기준점을 먼저 기록하세요." >&2
echo "상태 확인: sh scripts/migrate-production-db.sh status" >&2
exit 1
fi
ensure_schema_migrations_table
applied_count=0
migration_files | while IFS= read -r file_name; do
if is_applied "$file_name"; then
continue
fi
echo "apply $file_name"
psql_exec -f "$CONTAINER_MIGRATIONS_DIR/$file_name"
run_sql "INSERT INTO $SCHEMA_MIGRATIONS_TABLE (file_name) VALUES ('$file_name') ON CONFLICT (file_name) DO NOTHING;"
applied_count=$((applied_count + 1))
done
echo "마이그레이션 실행 완료"
}
ensure_database_ready
case "$MODE" in
status)
print_status
;;
baseline)
baseline_migrations
;;
migrate)
migrate_database
;;
*)
echo "사용법: sh scripts/migrate-production-db.sh [status|baseline|migrate] [기준번호]" >&2
exit 1
;;
esac