pipeline { agent any environment { DEPLOY_DIR = '/volume1/docker/autostock' ENV_FILE = "${DEPLOY_DIR}/.env" // Dockerfile 의 pip/npm 캐시 마운트(RUN --mount=type=cache)를 쓰려면 BuildKit 필요 DOCKER_BUILDKIT = '1' COMPOSE_DOCKER_CLI_BUILD = '1' } options { timeout(time: 30, unit: 'MINUTES') disableConcurrentBuilds() } stages { stage('Checkout') { steps { checkout scm } } stage('Validate') { steps { sh ''' echo "=== Syntax Check ===" python3 -c " import ast, pathlib errors = [] for f in pathlib.Path('backend/app').rglob('*.py'): try: ast.parse(f.read_text()) except SyntaxError as e: errors.append(f'{f}: {e}') for f in pathlib.Path('mcp-server').glob('*.py'): try: ast.parse(f.read_text()) except SyntaxError as e: errors.append(f'{f}: {e}') if errors: print('\\n'.join(errors)) exit(1) print('All files OK') " ''' } } stage('Sync & Build') { steps { sh """ echo "=== Syncing source to deploy directory ===" mkdir -p ${DEPLOY_DIR} # bind mount 대상 디렉터리 사전 생성 (없으면 docker 가 Bind mount failed 로 중단) mkdir -p ${DEPLOY_DIR}/logs ${DEPLOY_DIR}/token_cache chmod 777 ${DEPLOY_DIR}/token_cache 2>/dev/null || true # .env 존재 확인 if [ ! -f "${ENV_FILE}" ]; then echo "WARNING: ${ENV_FILE} not found. Creating template..." cp deploy/setup-nas.sh ${DEPLOY_DIR}/ 2>/dev/null || true fi # 소스 동기화 (backend, mcp-server, frontend, docker-compose.yml, docker/) cp docker-compose.yml ${DEPLOY_DIR}/ cp -r docker/ ${DEPLOY_DIR}/ 2>/dev/null || true # health-watch 사이드카가 ./scripts/health_watch.py 를 마운트한다 — 없으면 컨테이너가 Created 로 멈춘다(2026-10-12) # 이전 배포(0feaa61 이전)에 파일이 없어 Docker 가 마운트 소스를 '디렉터리'로 만들어 둔 경우, # cp 가 그 디렉터리 안쪽에 복사돼 마운트는 여전히 디렉터리 → 사이드카 python 실행 실패(|| true 로 은폐). 먼저 제거. if [ -d "${DEPLOY_DIR}/scripts/health_watch.py" ]; then rm -rf "${DEPLOY_DIR}/scripts/health_watch.py" || echo "WARN: health_watch.py 디렉터리 제거 실패 — NAS 에서 수동 삭제 필요"; fi mkdir -p ${DEPLOY_DIR}/scripts && cp scripts/health_watch.py ${DEPLOY_DIR}/scripts/ rsync -a --delete backend/ ${DEPLOY_DIR}/backend/ || cp -r backend/ ${DEPLOY_DIR}/backend/ rsync -a --delete mcp-server/ ${DEPLOY_DIR}/mcp-server/ || cp -r mcp-server/ ${DEPLOY_DIR}/mcp-server/ rsync -a --delete frontend/ ${DEPLOY_DIR}/frontend/ || cp -r frontend/ ${DEPLOY_DIR}/frontend/ echo "=== Building Docker Images ===" cd ${DEPLOY_DIR} docker-compose build backend mcp-server frontend """ } } // H1: 마이그레이션은 반드시 Deploy(백엔드 재기동) 이전에 끝나야 한다. // `create_all()`은 신규 테이블만 생성하고 기존 테이블 컬럼 추가는 하지 않으므로, // 새 이미지를 먼저 올리면(구 스키마 + 신규 컬럼을 매핑하는 신규 코드) 레거시 경로 // 전체가 UndefinedColumnError로 실패한다. 마이그레이션 실패 시 파이프라인을 중단한다 // (기존 backend 컨테이너는 아직 중지 전이라 서비스 영향 없음). stage('DB Migration') { steps { sh """ cd ${DEPLOY_DIR} # [2026-10-02] 운영 이미지의 파이썬(3.11)으로 문법 검사 — Validate 단계는 NAS 시스템 # python3 로 돌아 3.12 전용 문법(PEP 701 f-string 등)을 놓친다. 실패 시 마이그레이션·배포 전에 중단. docker-compose run --rm --no-deps backend python -m compileall -q app # N1: create_all 전용으로 운영돼 alembic_version 이 비어 있으면 upgrade head 가 # 0001 부터 실행돼 기존 컬럼과 충돌한다. 이력이 없으면 현 스키마(users 테이블까지 # create_all 로 존재) 기준인 0008 로 stamp 한 뒤 0009+ 만 적용한다. CURRENT=\$(docker-compose run --rm --no-deps backend alembic current 2>/dev/null | grep -oE '^[0-9a-f]{4,}' || true) if [ -z "\$CURRENT" ]; then echo "alembic 이력 없음 → stamp 0008" docker-compose run --rm --no-deps backend alembic stamp 0008 else echo "alembic current: \$CURRENT" fi docker-compose run --rm --no-deps backend alembic upgrade head """ } } stage('Deploy') { steps { sh """ cd ${DEPLOY_DIR} echo "=== Rolling Deploy (앱만 재시작, DB/Redis 유지) ===" docker-compose stop backend mcp-server frontend 2>/dev/null || true docker-compose up -d echo "=== Waiting for health check ===" sleep 15 # [2026-10-02] 헬스 실패를 빌드 실패로 — 이전엔 루프가 끝나도 성공 처리됐다. # /health 는 DB/Redis 장애 시 200+"degraded" 를 주므로 status 까지 확인한다. HEALTHY=0 for i in 1 2 3 4 5 6 7 8 9 10; do if curl -sf http://localhost:8000/health 2>/dev/null | grep -q '"status":"healthy"'; then echo "Backend is healthy!" HEALTHY=1 break fi echo "Waiting for backend... (\$i/10)" sleep 5 done if [ "\$HEALTHY" != "1" ]; then echo "❌ Backend health check failed" docker-compose logs --tail=80 backend || true exit 1 fi docker-compose ps """ } } } post { success { echo 'Deployment successful!' } failure { echo 'Deployment failed!' sh """ cd ${DEPLOY_DIR} 2>/dev/null && docker-compose logs --tail=30 backend mcp-server 2>/dev/null || true """ } always { cleanWs() } } }