da11a9c29f
- Git 저장소/배포 work-tree를 /Users/aidev/apps/goldenChart로 통일 - 레거시 dev 경로는 심볼릭 링크로 흡수, deploy.sh에 소스 검증·no-cache 옵션 - server-rebuild-exdev.sh, verify-source-sync.sh 추가 Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
5.5 KiB
Bash
Executable File
157 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ==============================================================================
|
|
# GoldenChart 공통 배포 스크립트 v2 — Docker Compose
|
|
# 호출: Git post-receive (Track A) | Jenkins (Track B)
|
|
# 설치: /Volumes/ADATA/git/deploy.sh
|
|
# ==============================================================================
|
|
set -euo pipefail
|
|
|
|
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}"
|
|
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
|
|
BRANCH="${BRANCH:-main}"
|
|
DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${WORK_TREE}/docker-compose.yml}"
|
|
DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/deploy.log}"
|
|
LOG_TAG="[goldenChart-deploy]"
|
|
SERVICE_NAME="${SERVICE_NAME:-all}"
|
|
DOCKER_NO_CACHE="${DOCKER_NO_CACHE:-0}"
|
|
BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://localhost:8080/api/indicators/list}"
|
|
FRONTEND_URL="${FRONTEND_URL:-http://localhost:80}"
|
|
# 단일 배포 경로 (다른 work-tree 사용 금지)
|
|
LEGACY_WORK_TREE="/Users/aidev/dev/goldenChart"
|
|
|
|
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
|
|
|
|
mkdir -p "$(dirname "$DEPLOY_LOG")"
|
|
exec >> "$DEPLOY_LOG" 2>&1
|
|
|
|
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_TAG $*"; }
|
|
info() { log "[INFO] $*"; }
|
|
warn() { log "[WARN] $*"; }
|
|
err() { log "[ERROR] $*"; exit 1; }
|
|
|
|
log "======================================================"
|
|
log " GoldenChart 배포 시작 (v2 Docker)"
|
|
log " 서비스 : $SERVICE_NAME"
|
|
log " 브랜치 : $BRANCH"
|
|
log " 작업트리: $WORK_TREE"
|
|
log " compose : $DOCKER_COMPOSE_FILE"
|
|
log " 실행자 : $(whoami)"
|
|
log "======================================================"
|
|
|
|
LOCK_FILE="/tmp/goldenchart_deploy.lock"
|
|
if [ -f "$LOCK_FILE" ]; then
|
|
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
|
|
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
|
|
err "다른 배포 진행 중 (PID: $LOCK_PID)"
|
|
fi
|
|
rm -f "$LOCK_FILE"
|
|
fi
|
|
echo $$ > "$LOCK_FILE"
|
|
trap 'rm -f "$LOCK_FILE"; log "락 파일 해제"' EXIT
|
|
|
|
# Phase 1
|
|
info "[Phase 1] Git checkout"
|
|
[ ! -d "$WORK_TREE" ] && mkdir -p "$WORK_TREE"
|
|
[ ! -d "$GIT_DIR" ] && err "GIT_DIR 없음: $GIT_DIR"
|
|
git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f "$BRANCH"
|
|
COMMIT="$(git --git-dir="$GIT_DIR" rev-parse HEAD)"
|
|
info "체크아웃 완료: $(git --git-dir="$GIT_DIR" rev-parse --short HEAD)"
|
|
|
|
# work-tree 에 중첩 .git 금지 (bare checkout 과 혼선 방지)
|
|
if [ -d "$WORK_TREE/.git" ]; then
|
|
err "work-tree에 중첩 .git 존재 — 제거 후 재배포: rm -rf $WORK_TREE/.git"
|
|
fi
|
|
WT_HEAD="$(git --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" rev-parse HEAD)"
|
|
[ "$WT_HEAD" = "$COMMIT" ] || err "work-tree HEAD 불일치: $WT_HEAD != $COMMIT"
|
|
if [ -f "$WORK_TREE/frontend/src/utils/ChartManager.ts" ]; then
|
|
info "소스 검증 ChartManager.sha256=$(shasum -a 256 "$WORK_TREE/frontend/src/utils/ChartManager.ts" | awk '{print $1}')"
|
|
fi
|
|
if [ -e "$LEGACY_WORK_TREE" ] && [ ! -L "$LEGACY_WORK_TREE" ]; then
|
|
warn "레거시 경로 존재(단일화 권장): $LEGACY_WORK_TREE — 배포는 $WORK_TREE 만 사용"
|
|
fi
|
|
|
|
[ ! -f "$DOCKER_COMPOSE_FILE" ] && err "docker-compose.yml 없음: $DOCKER_COMPOSE_FILE"
|
|
cd "$WORK_TREE"
|
|
|
|
# Phase 2 — Docker 이미지 빌드 (호스트 npm 사용 안 함 → postcss 홈 디렉터리 간섭 방지)
|
|
info "[Phase 2] Docker 이미지 빌드 ($SERVICE_NAME)"
|
|
|
|
_dc_build() {
|
|
local svc="$1"
|
|
if [ "$DOCKER_NO_CACHE" = "1" ]; then
|
|
info "docker compose build --no-cache $svc"
|
|
docker compose -f "$DOCKER_COMPOSE_FILE" build --no-cache "$svc"
|
|
else
|
|
info "docker compose build $svc"
|
|
docker compose -f "$DOCKER_COMPOSE_FILE" build "$svc"
|
|
fi
|
|
}
|
|
|
|
build_frontend() { _dc_build frontend; }
|
|
build_backend() { _dc_build backend; }
|
|
|
|
case "$SERVICE_NAME" in
|
|
frontend) build_frontend ;;
|
|
backend) build_backend ;;
|
|
all)
|
|
build_backend
|
|
build_frontend
|
|
;;
|
|
docker-only) info "빌드 스킵 (docker-only)" ;;
|
|
python|ai-agent)
|
|
warn "goldenChart는 PM2 python 배포 미사용 — docker-only 권장"
|
|
;;
|
|
*) warn "알 수 없는 SERVICE_NAME=$SERVICE_NAME" ;;
|
|
esac
|
|
|
|
# Phase 3
|
|
info "[Phase 3] Docker Compose up"
|
|
|
|
case "$SERVICE_NAME" in
|
|
frontend)
|
|
docker compose -f "$DOCKER_COMPOSE_FILE" up -d --no-deps frontend
|
|
;;
|
|
backend)
|
|
docker compose -f "$DOCKER_COMPOSE_FILE" up -d mysql
|
|
docker compose -f "$DOCKER_COMPOSE_FILE" up -d --no-deps backend
|
|
;;
|
|
all|docker-only)
|
|
docker compose -f "$DOCKER_COMPOSE_FILE" up -d
|
|
;;
|
|
esac
|
|
|
|
# Phase 4
|
|
info "[Phase 4] 헬스체크"
|
|
|
|
check_url() {
|
|
local name="$1" url="$2" retries="${3:-12}" interval="${4:-5}"
|
|
local i code
|
|
for i in $(seq 1 "$retries"); do
|
|
code=$(curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")
|
|
if [[ "$code" =~ ^2 ]]; then
|
|
info "$name OK (HTTP $code)"
|
|
return 0
|
|
fi
|
|
warn "$name 대기 ($i/$retries) HTTP $code"
|
|
sleep "$interval"
|
|
done
|
|
warn "$name 헬스체크 실패: $url"
|
|
return 1
|
|
}
|
|
|
|
case "$SERVICE_NAME" in
|
|
frontend) check_url "Frontend" "$FRONTEND_URL" || true ;;
|
|
backend) check_url "Backend" "$BACKEND_HEALTH_URL" || true ;;
|
|
all|docker-only)
|
|
check_url "Frontend" "$FRONTEND_URL" || true
|
|
check_url "Backend" "$BACKEND_HEALTH_URL" || true
|
|
;;
|
|
esac
|
|
|
|
log "======================================================"
|
|
log " 배포 완료: $(date '+%Y-%m-%d %H:%M:%S')"
|
|
log " 커밋: $(git --git-dir="$GIT_DIR" log --oneline -1 2>/dev/null || echo N/A)"
|
|
log "======================================================"
|
|
|
|
docker compose -f "$DOCKER_COMPOSE_FILE" ps 2>/dev/null || docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|