diff --git a/deploy-home.sh b/deploy-home.sh index abe6f89..2709251 100755 --- a/deploy-home.sh +++ b/deploy-home.sh @@ -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_USER="${REMOTE_USER:-aidev}" 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}" BACKEND_PORT="${BACKEND_PORT:-8080}" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index f34c9d1..12d2739 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -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}" 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}" @@ -51,23 +54,41 @@ 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)" -build_frontend() { - info "docker compose build frontend" - docker compose -f "$DOCKER_COMPOSE_FILE" build frontend +_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_backend() { - info "docker compose build backend" - docker compose -f "$DOCKER_COMPOSE_FILE" build backend -} +build_frontend() { _dc_build frontend; } +build_backend() { _dc_build backend; } case "$SERVICE_NAME" in frontend) build_frontend ;; diff --git a/scripts/post-receive b/scripts/post-receive new file mode 100755 index 0000000..4c300ed --- /dev/null +++ b/scripts/post-receive @@ -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 "" diff --git a/scripts/server-rebuild-exdev.sh b/scripts/server-rebuild-exdev.sh new file mode 100755 index 0000000..ab11c87 --- /dev/null +++ b/scripts/server-rebuild-exdev.sh @@ -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' + + + GoldenChart — Track B (deploy.sh, file:// bare repo) + false + + + 2 + + + file:///Volumes/ADATA/git/goldenChart.git + + + + + */main + + + false + + + + true + false + false + false + + false + + + #!/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 + + + + + + +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 && git push origin main" +log "============================================================" diff --git a/scripts/verify-source-sync.sh b/scripts/verify-source-sync.sh new file mode 100755 index 0000000..9161315 --- /dev/null +++ b/scripts/verify-source-sync.sh @@ -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; }