chore: exdev 배포 경로 단일화 및 서버 재구축 스크립트 추가

- 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>
This commit is contained in:
Macbook
2026-06-02 22:55:12 +09:00
parent 85285f65c7
commit da11a9c29f
5 changed files with 288 additions and 8 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BO
REMOTE_HOST="${REMOTE_HOST:-exdev.co.kr}" REMOTE_HOST="${REMOTE_HOST:-exdev.co.kr}"
REMOTE_USER="${REMOTE_USER:-aidev}" REMOTE_USER="${REMOTE_USER:-aidev}"
REMOTE_PORT="${REMOTE_PORT:-22}" REMOTE_PORT="${REMOTE_PORT:-22}"
REMOTE_PROJECT_DIR="${REMOTE_PROJECT_DIR:-/Users/aidev/dev/goldenChart}" REMOTE_PROJECT_DIR="${REMOTE_PROJECT_DIR:-/Users/aidev/apps/goldenChart}"
FRONTEND_PORT="${FRONTEND_PORT:-80}" FRONTEND_PORT="${FRONTEND_PORT:-80}"
BACKEND_PORT="${BACKEND_PORT:-8080}" BACKEND_PORT="${BACKEND_PORT:-8080}"
+28 -7
View File
@@ -13,8 +13,11 @@ DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${WORK_TREE}/docker-compose.yml}"
DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/deploy.log}" DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/deploy.log}"
LOG_TAG="[goldenChart-deploy]" LOG_TAG="[goldenChart-deploy]"
SERVICE_NAME="${SERVICE_NAME:-all}" SERVICE_NAME="${SERVICE_NAME:-all}"
DOCKER_NO_CACHE="${DOCKER_NO_CACHE:-0}"
BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://localhost:8080/api/indicators/list}" BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://localhost:8080/api/indicators/list}"
FRONTEND_URL="${FRONTEND_URL:-http://localhost:80}" 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}" export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
@@ -51,23 +54,41 @@ info "[Phase 1] Git checkout"
[ ! -d "$WORK_TREE" ] && mkdir -p "$WORK_TREE" [ ! -d "$WORK_TREE" ] && mkdir -p "$WORK_TREE"
[ ! -d "$GIT_DIR" ] && err "GIT_DIR 없음: $GIT_DIR" [ ! -d "$GIT_DIR" ] && err "GIT_DIR 없음: $GIT_DIR"
git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f "$BRANCH" 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)" 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" [ ! -f "$DOCKER_COMPOSE_FILE" ] && err "docker-compose.yml 없음: $DOCKER_COMPOSE_FILE"
cd "$WORK_TREE" cd "$WORK_TREE"
# Phase 2 — Docker 이미지 빌드 (호스트 npm 사용 안 함 → postcss 홈 디렉터리 간섭 방지) # Phase 2 — Docker 이미지 빌드 (호스트 npm 사용 안 함 → postcss 홈 디렉터리 간섭 방지)
info "[Phase 2] Docker 이미지 빌드 ($SERVICE_NAME)" info "[Phase 2] Docker 이미지 빌드 ($SERVICE_NAME)"
build_frontend() { _dc_build() {
info "docker compose build frontend" local svc="$1"
docker compose -f "$DOCKER_COMPOSE_FILE" build frontend 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_backend() { build_frontend() { _dc_build frontend; }
info "docker compose build backend" build_backend() { _dc_build backend; }
docker compose -f "$DOCKER_COMPOSE_FILE" build backend
}
case "$SERVICE_NAME" in case "$SERVICE_NAME" in
frontend) build_frontend ;; frontend) build_frontend ;;
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# GoldenChart post-receive — Track A (git push main → deploy.sh)
# 설치: cp scripts/post-receive /Volumes/ADATA/git/goldenChart.git/hooks/post-receive
set -euo pipefail
GIT_DIR="/Volumes/ADATA/git/goldenChart.git"
DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy.sh"
SUMMARY_SCRIPT="/Volumes/ADATA/git/git-push-summary.sh"
DEPLOY_LOG="/Volumes/ADATA/logs/deploy/deploy.log"
SUMMARY_DIR="/Volumes/ADATA/logs/deploy/push-summaries"
HOOK_TAG="[post-receive]"
mkdir -p "$(dirname "$DEPLOY_LOG")" "$SUMMARY_DIR"
echo ""
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG =============================="
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Track A — git push 수신"
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG =============================="
while read -r OLDREV NEWREV REFNAME; do
BRANCH_NAME="${REFNAME#refs/heads/}"
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 브랜치: $BRANCH_NAME ($OLDREV → $NEWREV)"
if [ -x "$SUMMARY_SCRIPT" ]; then
SUMMARY_FILE="$("$SUMMARY_SCRIPT" "$GIT_DIR" "$OLDREV" "$NEWREV" "$REFNAME" "$SUMMARY_DIR" || true)"
if [ -n "${SUMMARY_FILE:-}" ] && [ -f "$SUMMARY_FILE" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 변경 요약: $SUMMARY_FILE"
head -20 "$SUMMARY_FILE" | sed "s/^/$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG /"
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] 요약 스크립트 없음: $SUMMARY_SCRIPT"
fi
if [ "$BRANCH_NAME" = "main" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG main → deploy.sh 실행"
export SERVICE_NAME="${SERVICE_NAME:-all}"
"$DEPLOY_SCRIPT" 2>&1 | tee -a "$DEPLOY_LOG"
EXIT_CODE=${PIPESTATUS[0]}
if [ "$EXIT_CODE" -ne 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 실패 (exit: $EXIT_CODE)"
exit "$EXIT_CODE"
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 배포 완료"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG '$BRANCH_NAME' — 배포 스킵 (main만)"
fi
done
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG =============================="
echo ""
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
# ==============================================================================
# exdev.co.kr — Git bare + work-tree + Jenkins 완전 재구축
# 실행 위치: 서버 (aidev@exdev.co.kr) 또는 로컬에서 ssh로 원격 실행
#
# 유지 경로 (현재 운영과 동일):
# GIT_DIR = /Volumes/ADATA/git/goldenChart.git
# WORK_TREE = /Users/aidev/apps/goldenChart
# DEPLOY_SCRIPT= /Volumes/ADATA/git/deploy.sh
# DEPLOY_LOG = /Volumes/ADATA/logs/deploy/deploy.log
# Jenkins = http://127.0.0.1:8090 job: goldenChart-Pipeline
# ==============================================================================
set -euo pipefail
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
GIT_DIR="/Volumes/ADATA/git/goldenChart.git"
WORK_TREE="/Users/aidev/apps/goldenChart"
DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy.sh"
DEPLOY_LOG="/Volumes/ADATA/logs/deploy/deploy.log"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
JENKINS_HOME="${JENKINS_HOME:-$HOME/.jenkins}"
JENKINS_JOB="goldenChart-Pipeline"
STAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_ROOT="/Volumes/ADATA/backup/goldenChart-rebuild-${STAMP}"
log() { echo "[rebuild $(date '+%H:%M:%S')] $*"; }
log "백업 디렉터리: $BACKUP_ROOT"
mkdir -p "$BACKUP_ROOT" "$(dirname "$DEPLOY_LOG")" /Volumes/ADATA/logs/deploy/push-summaries
# ── 1. Jenkins Job 제거 (데이터는 유지, job만 초기화) ─────────────────────
log "Jenkins job 제거: $JENKINS_JOB"
rm -rf "${JENKINS_HOME}/jobs/${JENKINS_JOB}"
rm -f "${JENKINS_HOME}/jobs/${JENKINS_JOB}.xml" 2>/dev/null || true
# ── 2. work-tree 백업 (.env·데이터 보존) ───────────────────────────────────
if [ -d "$WORK_TREE" ]; then
log "work-tree 백업 → $BACKUP_ROOT/work-tree-snapshot"
mkdir -p "$BACKUP_ROOT/work-tree-snapshot"
[ -f "$WORK_TREE/.env" ] && cp -a "$WORK_TREE/.env" "$BACKUP_ROOT/.env"
# embedded .git 제거 대상 — 백업만
if [ -d "$WORK_TREE/.git" ]; then
log "work-tree 내 중첩 .git 백업 (배포 혼선 원인)"
cp -a "$WORK_TREE/.git" "$BACKUP_ROOT/work-tree-snapshot/dot-git-nested"
fi
fi
# ── 3. bare 저장소 제거 후 재생성 ───────────────────────────────────────────
if [ -d "$GIT_DIR" ]; then
log "bare 저장소 백업 → $BACKUP_ROOT/goldenChart.git.tgz"
tar -czf "$BACKUP_ROOT/goldenChart.git.tgz" -C "$(dirname "$GIT_DIR")" "$(basename "$GIT_DIR")" 2>/dev/null || true
log "bare 저장소 제거: $GIT_DIR"
rm -rf "$GIT_DIR"
fi
log "bare 저장소 초기화"
git init --bare "$GIT_DIR"
git --git-dir="$GIT_DIR" symbolic-ref HEAD refs/heads/main
# ── 4. 레거시 work-tree 제거·단일 경로 고정 ─────────────────────────────────
LEGACY_WORK_TREE="/Users/aidev/dev/goldenChart"
if [ -e "$LEGACY_WORK_TREE" ] || [ -L "$LEGACY_WORK_TREE" ]; then
log "레거시 경로 백업·이동: $LEGACY_WORK_TREE"
if [ -d "$LEGACY_WORK_TREE" ] && [ ! -L "$LEGACY_WORK_TREE" ]; then
tar -czf "$BACKUP_ROOT/legacy-dev-goldenChart.tgz" -C "$(dirname "$LEGACY_WORK_TREE")" "$(basename "$LEGACY_WORK_TREE")" 2>/dev/null || true
mv "$LEGACY_WORK_TREE" "$BACKUP_ROOT/legacy-dev-goldenChart-dir" 2>/dev/null || true
else
rm -f "$LEGACY_WORK_TREE" 2>/dev/null || true
fi
fi
ln -sfn "$WORK_TREE" "$LEGACY_WORK_TREE"
log "$LEGACY_WORK_TREE$WORK_TREE (심볼릭 링크)"
# ── 5. work-tree 정리 (중첩 .git 제거, 소스만 비움) ───────────────────────
log "work-tree 정리: $WORK_TREE"
mkdir -p "$WORK_TREE"
if [ -d "$WORK_TREE/.git" ]; then
rm -rf "$WORK_TREE/.git"
log " → 중첩 .git 제거 완료"
fi
# 소스 파일만 삭제 (.env·data·docker 볼륨 경로 유지)
find "$WORK_TREE" -mindepth 1 -maxdepth 1 \
! -name '.env' \
! -name '.env.*' \
! -name 'data' \
-exec rm -rf {} + 2>/dev/null || true
[ -f "$BACKUP_ROOT/.env" ] && cp -a "$BACKUP_ROOT/.env" "$WORK_TREE/.env" && log " → .env 복원"
# ── 6. deploy.sh · post-receive 설치 ───────────────────────────────────────
log "deploy.sh 설치"
cp -f "$REPO_ROOT/scripts/deploy.sh" "$DEPLOY_SCRIPT"
chmod +x "$DEPLOY_SCRIPT"
log "post-receive 훅 설치"
cp -f "$REPO_ROOT/scripts/post-receive" "$GIT_DIR/hooks/post-receive"
chmod +x "$GIT_DIR/hooks/post-receive"
if [ -x /Volumes/ADATA/git/git-push-summary.sh ]; then
log "git-push-summary.sh 유지"
else
log "[WARN] /Volumes/ADATA/git/git-push-summary.sh 없음 — push 요약 생략"
fi
# ── 7. Jenkins Job 재생성 ──────────────────────────────────────────────────
log "Jenkins job 생성: $JENKINS_JOB"
mkdir -p "${JENKINS_HOME}/jobs/${JENKINS_JOB}"
cat > "${JENKINS_HOME}/jobs/${JENKINS_JOB}/config.xml" << 'JENKINS_XML'
<?xml version='1.1' encoding='UTF-8'?>
<project>
<description>GoldenChart — Track B (deploy.sh, file:// bare repo)</description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.plugins.git.GitSCM" plugin="git@5.0.0">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>file:///Volumes/ADATA/git/goldenChart.git</url>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/main</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="empty-list"/>
<extensions/>
</scm>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers/>
<concurrentBuild>false</concurrentBuild>
<builders>
<hudson.tasks.Shell>
<command>#!/bin/bash
set -euo pipefail
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:$PATH"
echo "=== goldenChart Jenkins Build #${BUILD_NUMBER} ==="
echo "시각: $(date)"
SERVICE_NAME="${SERVICE_NAME:-all}" /Volumes/ADATA/git/deploy.sh
</command>
</hudson.tasks.Shell>
</builders>
<publishers/>
<buildWrappers/>
</project>
JENKINS_XML
# Jenkins 설정 리로드 (실패해도 job 파일은 생성됨)
if command -v jenkins-cli >/dev/null 2>&1; then
jenkins-cli reload-configuration 2>/dev/null || true
fi
if curl -sf -o /dev/null "http://127.0.0.1:8090/login" 2>/dev/null; then
curl -sf -X POST "http://127.0.0.1:8090/reload" 2>/dev/null || true
fi
rm -f /tmp/goldenchart_deploy.lock 2>/dev/null || true
log "============================================================"
log "서버 Git/Jenkins 재구축 완료 (소스는 아직 비어 있음)"
log " GIT_DIR : $GIT_DIR"
log " WORK_TREE : $WORK_TREE (중첩 .git 없음)"
log " Jenkins : $JENKINS_JOB → file://$GIT_DIR"
log ""
log "다음: 로컬에서 실행"
log " cd <goldenChart> && git push origin main"
log "============================================================"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# 로컬 ↔ 서버 work-tree 소스 동일성 검증
set -euo pipefail
REMOTE="${REMOTE:-aidev@exdev.co.kr}"
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
LOCAL_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FILES=(
frontend/src/utils/ChartManager.ts
frontend/src/utils/IchimokuCloudPlugin.ts
frontend/package-lock.json
scripts/deploy.sh
)
echo "=== 로컬 ↔ 서버 체크섬 ($REMOTE:$WORK_TREE) ==="
FAIL=0
for f in "${FILES[@]}"; do
L="$LOCAL_ROOT/$f"
[ -f "$L" ] || { echo "MISSING local $f"; FAIL=1; continue; }
LHASH=$(shasum -a 256 "$L" | awk '{print $1}')
RHASH=$(ssh -o BatchMode=yes "$REMOTE" "shasum -a 256 '$WORK_TREE/$f' 2>/dev/null" | awk '{print $1}')
if [ "$LHASH" = "$RHASH" ]; then
echo "OK $f"
else
echo "DIFF $f"
echo " local=$LHASH"
echo " remote=$RHASH"
FAIL=1
fi
done
echo "=== Git HEAD ==="
echo -n "local: "; git -C "$LOCAL_ROOT" rev-parse HEAD
echo -n "remote: "; ssh -o BatchMode=yes "$REMOTE" "git --git-dir=/Volumes/ADATA/git/goldenChart.git rev-parse HEAD"
echo ""
[ "$FAIL" -eq 0 ] && echo "검증 통과" || { echo "검증 실패"; exit 1; }