From fb4179eaa5d975b88d613e260f44646ec15925f7 Mon Sep 17 00:00:00 2001 From: bkyungee Date: Thu, 13 Aug 2026 13:47:23 +0900 Subject: [PATCH] Deploy: exdev sync 2026-08-13 13:47:23 --- git_fetch_all.sh | 980 +++++++++++++++---------------- local-dev/bin/rsync | 248 ++++---- server-dev/deploy.sh | 308 +++++----- server-dev/wait-remote-deploy.sh | 172 +++--- 4 files changed, 854 insertions(+), 854 deletions(-) diff --git a/git_fetch_all.sh b/git_fetch_all.sh index f7e1d0f..2f1f178 100755 --- a/git_fetch_all.sh +++ b/git_fetch_all.sh @@ -1,490 +1,490 @@ -#!/usr/bin/env bash -# 4개 submodule: 로컬 변경 → 공식 GitHub chatbot2.0-agent commit + push -# -# ★ 공식 GitHub(KoreaExpressWayCorp) 반영은 이 스크립트로만 수행한다. -# start-all-server.sh / push-to-gitea.sh 는 개발서버(Gitea/exdev)만 다룬다. -# -# ./git_fetch_all.sh # fetch/merge + commit + push (공식 GitHub) -# ./git_fetch_all.sh --init # submodule 미초기화 시 clone 포함 -# ./git_fetch_all.sh --dry-run # 실행 없이 상태만 출력 -# ./git_fetch_all.sh --no-push # commit까지, push 생략 -# ./git_fetch_all.sh --no-commit # fetch/merge만 (commit·push 생략) -# -# ★ 운영(GitHub) push 전용 — 개발서버(exdev) 주소를 커밋/push 하지 않는다. -# start-all-server.sh 가 런타임 ENV 로만 exdev(127.0.0.1 Mongo / localhost Oracle TROAD / -# host.docker.internal RAG)를 적용한다. -# 이 스크립트는 commit 직전에 운영 기본값(172.16.*)을 파일에 복원한 뒤 push 한다. -# (.env / EXDEV_LOCAL / server-dev 오버레이는 GitHub 대상이 아님) -# -# 대상 (.gitmodules) — 모두 branch=chatbot2.0-agent (main 사용 안 함): -# chatbotApi-chatbot2.0-agent -# chatbotApp-chatbot2.0-agent -# exAiChatBot-chatbot2.0-agent -# kakaoChatbotSkill-chatbot2.0-agent -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# bare repo 배포 시 상위에서 GIT_DIR 이 export 되면 submodule git 이 깨짐 -MONOREPO_GIT_DIR="${MONOREPO_GIT_DIR:-${GIT_DIR:-}}" -unset GIT_DIR GIT_WORK_TREE - -INIT=false -DRY_RUN=false -DO_COMMIT=true -DO_PUSH=true -TARGET_BRANCH="chatbot2.0-agent" - -# 명시 대상 (순서 고정). .gitmodules 와 일치해야 함. -TARGET_PATHS=( - "chatbotApi-chatbot2.0-agent" - "chatbotApp-chatbot2.0-agent" - "exAiChatBot-chatbot2.0-agent" - "kakaoChatbotSkill-chatbot2.0-agent" -) - -for arg in "$@"; do - case "$arg" in - --init) INIT=true ;; - --dry-run) DRY_RUN=true ;; - --no-commit) DO_COMMIT=false; DO_PUSH=false ;; - --no-push) DO_PUSH=false ;; - -h|--help) - sed -n '2,22p' "$0" - exit 0 - ;; - *) - echo "알 수 없는 옵션: $arg (--help)" - exit 1 - ;; - esac -done - -log() { echo "[git_fetch_all] $*"; } -warn() { echo "[git_fetch_all] ⚠️ $*" >&2; } -ok() { echo "[git_fetch_all] ✅ $*"; } - -cd "${ROOT_DIR}" - -if [[ ! -f .gitmodules ]]; then - echo "ERROR: .gitmodules 없음 — submodule 미설정" - exit 1 -fi - -resolve_monorepo_git_dir() { - if [[ -n "${MONOREPO_GIT_DIR}" && -d "${MONOREPO_GIT_DIR}" ]]; then - echo "${MONOREPO_GIT_DIR}" - elif git -C "${ROOT_DIR}" rev-parse --git-dir >/dev/null 2>&1; then - git -C "${ROOT_DIR}" rev-parse --git-dir - fi -} - -submodule_pinned_sha() { - local path="$1" - local git_dir - git_dir="$(resolve_monorepo_git_dir || true)" - [[ -n "${git_dir}" ]] || return 0 - git --git-dir="${git_dir}" ls-tree HEAD "${path}" 2>/dev/null | awk '{print $3}' -} - -clone_submodule_repo() { - local path="$1" url="$2" branch="$3" - local target="${ROOT_DIR}/${path}" - local sha - - sha="$(submodule_pinned_sha "${path}" || true)" - log " clone ${url} → ${path} (branch=${branch})" - rm -rf "${target}" - if ! git clone --branch "${branch}" --single-branch "${url}" "${target}"; then - warn "${path} — clone 실패 (서버 GitHub 미접속 시 sync-submodules-rsync.sh 사용)" - return 1 - fi - if [[ -n "${sha}" ]]; then - git -C "${target}" checkout "${sha}" 2>/dev/null || true - fi -} - -restore_monorepo_overlays() { - if [[ -f "${ROOT_DIR}/local-dev/docker-compose.rag.local.yml" ]]; then - cp "${ROOT_DIR}/local-dev/docker-compose.rag.local.yml" \ - "${ROOT_DIR}/exAiChatBot-chatbot2.0-agent/docker-compose.local.yml" - log "복원: exAiChatBot/docker-compose.local.yml (monorepo overlay)" - fi - if [[ -f "${ROOT_DIR}/server-dev/docker-compose.rag.server.yml" ]]; then - cp "${ROOT_DIR}/server-dev/docker-compose.rag.server.yml" \ - "${ROOT_DIR}/exAiChatBot-chatbot2.0-agent/docker-compose.server.yml" - log "복원: exAiChatBot/docker-compose.server.yml (monorepo overlay)" - fi -} - -# GitHub push 전: exdev 런타임 주소를 커밋하지 않고 운영 기본값(172.16.180.130) 유지 -# - 런타임(exdev): start-all-server.sh ENV / .env / EXDEV_LOCAL=true → 127.0.0.1·host.docker.internal -# - 커밋/push: application.yml·env.template·chatbot_tool_client.py 기본값을 운영 IP로 고정 -PROD_WAS_HOST="172.16.180.130" -PROD_WAS_API="http://${PROD_WAS_HOST}:8086/api" -PROD_RAG_CHAT="http://${PROD_WAS_HOST}:28012/agent/chat" -PROD_MONGO_URI="mongodb://exDev:ehfhrhdtk!!@${PROD_WAS_HOST}:27017/chatbot?heartbeatFrequencyMS=30000&minPoolSize=8&maxIdleTimeMS=18000000" -# 운영 TROAD Oracle (start-all 은 SPRING_DATASOURCE_TROAD_* ENV 로 localhost 오버라이드) -PROD_TROAD_JDBC_URL="jdbc:oracle:thin:@172.16.163.76:1522/TROAD" -PROD_TROAD_USERNAME="TROAD_APP" -PROD_TROAD_PASSWORD='!ryxhdapp2025' - -restore_production_endpoints_before_commit() { - local path="$1" - local dir="${ROOT_DIR}/${path}" - - case "${path}" in - chatbotApi-chatbot2.0-agent) - local yml="${dir}/src/main/resources/application.yml" - if [[ -f "${yml}" ]]; then - # 수동/임시로 바꾼 로컬 Mongo URI 를 운영 기본값으로 복원 (start-all 은 ENV 로만 오버라이드) - # URI 의 @ 는 perl 치환부 배열 보간을 피하기 위해 ENV 로 전달 - PROD_MONGO_URI="${PROD_MONGO_URI}" perl -i -pe \ - 's|uri:\s*mongodb://\S+|uri: $ENV{PROD_MONGO_URI}|g' "${yml}" - # troad datasource 를 로컬 Oracle 로 바꿔 둔 경우 운영값 복원 - PROD_TROAD_JDBC_URL="${PROD_TROAD_JDBC_URL}" \ - PROD_TROAD_USERNAME="${PROD_TROAD_USERNAME}" \ - PROD_TROAD_PASSWORD="${PROD_TROAD_PASSWORD}" \ - perl -i -0pe ' - s|(troad:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+url:\s*)\S+|$1$ENV{PROD_TROAD_JDBC_URL}|g; - s|(troad:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+username:\s*)\S+|$1$ENV{PROD_TROAD_USERNAME}|g; - s|(troad:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+password:\s*).*|$1"$ENV{PROD_TROAD_PASSWORD}"|g; - ' "${yml}" - fi - ;; - kakaoChatbotSkill-chatbot2.0-agent) - local yml="${dir}/src/main/resources/application.yml" - if [[ -f "${yml}" ]]; then - # ${ENV:default} 형태의 default 만 운영 IP로 고정 (실행 시 ENV 로컬 오버라이드는 유지) - PROD_WAS_API="${PROD_WAS_API}" PROD_RAG_CHAT="${PROD_RAG_CHAT}" PROD_WAS_HOST="${PROD_WAS_HOST}" \ - perl -i -pe ' - s|CHATBOT_API_BASE_URL:[^}]+}|CHATBOT_API_BASE_URL:$ENV{PROD_WAS_API}/}|g; - s|RAG_AGENT_CHAT_URL:[^}]+}|RAG_AGENT_CHAT_URL:$ENV{PROD_RAG_CHAT}}|g; - s|base-url:\s*http://127\.0\.0\.1:8086/api/?|base-url: \${CHATBOT_API_BASE_URL:$ENV{PROD_WAS_API}/}|g; - s|base-url:\s*http://host\.docker\.internal:8086/api/?|base-url: \${CHATBOT_API_BASE_URL:$ENV{PROD_WAS_API}/}|g; - s|url:\s*http://127\.0\.0\.1:28012/agent/chat|url: \${RAG_AGENT_CHAT_URL:$ENV{PROD_RAG_CHAT}}|g; - s|url:\s*http://host\.docker\.internal:28012/agent/chat|url: \${RAG_AGENT_CHAT_URL:$ENV{PROD_RAG_CHAT}}|g; - ' "${yml}" - fi - ;; - exAiChatBot-chatbot2.0-agent) - if [[ -f "${dir}/env.template" ]]; then - PROD_WAS_API="${PROD_WAS_API}" perl -i -pe \ - 's|^CHATBOT_API_BASE_URL=.*|CHATBOT_API_BASE_URL=$ENV{PROD_WAS_API}|' "${dir}/env.template" - # EXDEV_LOCAL 은 커밋 템플릿에 넣지 않음 (server-dev 전용) - perl -i -ne 'print unless /^EXDEV_LOCAL=/' "${dir}/env.template" - fi - if [[ -f "${dir}/env.example" ]]; then - PROD_WAS_API="${PROD_WAS_API}" perl -i -pe \ - 's|^CHATBOT_API_BASE_URL=.*|CHATBOT_API_BASE_URL=$ENV{PROD_WAS_API}|' "${dir}/env.example" 2>/dev/null || true - perl -i -ne 'print unless /^EXDEV_LOCAL=/' "${dir}/env.example" 2>/dev/null || true - fi - if [[ -f "${dir}/scripts/agent/chatbot_tool_client.py" ]]; then - # 운영 기본값(_PROD_WAS_API / legacy getenv default)만 복원. - PROD_WAS_API="${PROD_WAS_API}" perl -i -pe ' - s|_PROD_WAS_API = "http://[^"]+"|_PROD_WAS_API = "$ENV{PROD_WAS_API}"|g; - s|os\.getenv\("TOOL_API_BASE_URL", "http://[^"]+"\)|os.getenv("TOOL_API_BASE_URL", "$ENV{PROD_WAS_API}")|g; - s|os\.getenv\("CHATBOT_API_BASE_URL", "http://[^"]+"\)|os.getenv("CHATBOT_API_BASE_URL", "$ENV{PROD_WAS_API}")|g; - ' "${dir}/scripts/agent/chatbot_tool_client.py" - fi - # monorepo overlay compose 는 push 대상에서 제외 (로컬 exdev 전용) - git -C "${dir}" checkout HEAD -- docker-compose.server.yml docker-compose.local.yml 2>/dev/null || true - # .env 는 gitignore — 스테이징 실수 방지 - git -C "${dir}" reset HEAD -- .env .env.local 2>/dev/null || true - rm -f "${dir}/.env.local" 2>/dev/null || true - ;; - esac -} - -# 복원 후에도 개발서버 전용 호스트가 커밋 대상 설정 파일에 남으면 push 중단 -assert_no_exdev_endpoints_for_push() { - local path="$1" - local dir="${ROOT_DIR}/${path}" - local hits="" - - case "${path}" in - chatbotApi-chatbot2.0-agent) - hits="$(grep -nE 'mongodb://(127\.0\.0\.1|localhost)|host\.docker\.internal|jdbc:oracle:thin:@(127\.0\.0\.1|localhost)' \ - "${dir}/src/main/resources/application.yml" 2>/dev/null || true)" - # troad 블록이 로컬 계정(OPR_MAIN)으로 바뀌었는지도 차단 - if awk ' - /^[[:space:]]*troad:[[:space:]]*$/ { in_troad=1; next } - in_troad && /^[[:space:]]*[a-zA-Z0-9_-]+:[[:space:]]*$/ && $0 !~ /^[[:space:]]*troad:/ { in_troad=0 } - in_troad && /username:[[:space:]]*OPR_MAIN/ { found=1 } - END { exit found ? 0 : 1 } - ' "${dir}/src/main/resources/application.yml" 2>/dev/null; then - hits+=$'\n'"troad.username=OPR_MAIN (exdev local Oracle — must not commit)" - fi - ;; - kakaoChatbotSkill-chatbot2.0-agent) - hits="$(grep -nE '127\.0\.0\.1:(8086|28012)|host\.docker\.internal:(8086|28012)' \ - "${dir}/src/main/resources/application.yml" 2>/dev/null || true)" - ;; - exAiChatBot-chatbot2.0-agent) - hits="$(grep -nE '^(CHATBOT_API_BASE_URL|TOOL_API_BASE_URL)=.*(127\.0\.0\.1|host\.docker\.internal)|EXDEV_LOCAL=' \ - "${dir}/env.template" "${dir}/env.example" 2>/dev/null || true)" - hits+="$(grep -nE 'getenv\(\"TOOL_API_BASE_URL\", \"http://(127\.0\.0\.1|host\.docker\.internal)' \ - "${dir}/scripts/agent/chatbot_tool_client.py" 2>/dev/null || true)" - ;; - esac - - if [[ -n "${hits}" ]]; then - warn "${path}: 개발서버(exdev) 주소가 남아 push 중단" - echo "${hits}" | sed 's/^/ /' - warn "start-all-server.sh 런타임 ENV 로만 exdev 주소를 쓰고, 소스 기본값은 운영 IP 를 유지하세요" - return 1 - fi - return 0 -} - -submodule_is_cloned() { - local path="$1" - [[ -e "${ROOT_DIR}/${path}/.git" ]] -} - -repo_is_dirty() { - ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]] -} - -ensure_branch() { - local branch="$1" - local git_dir - # submodule 은 .git 이 파일일 수 있음 → rev-parse 사용 - git_dir="$(git rev-parse --git-dir 2>/dev/null || echo .git)" - if [[ -d "${git_dir}/rebase-merge" || -d "${git_dir}/rebase-apply" ]]; then - warn "rebase 진행 중 — 수동 해결 후 재실행 (git rebase --continue / --abort)" - return 1 - fi - if [[ -f "${git_dir}/MERGE_HEAD" ]]; then - warn "merge 진행 중 — 수동 해결 후 재실행" - return 1 - fi - - local cur - cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo DETACHED)" - if [[ "${cur}" == "${branch}" ]]; then - return 0 - fi - - if git show-ref --verify --quiet "refs/heads/${branch}"; then - git checkout "${branch}" - elif git show-ref --verify --quiet "refs/remotes/origin/${branch}"; then - git checkout -B "${branch}" "origin/${branch}" - else - git checkout -B "${branch}" - fi -} - -sync_one() { - local path="$1" - local url branch old new stashed=0 ahead - - url="$(git config -f .gitmodules "submodule.${path}.url" 2>/dev/null || true)" - if [[ -z "${url}" ]]; then - warn "${path} — .gitmodules 에 없음, 건너뜀" - return 1 - fi - - # 사용자 요구: main 금지, chatbot2.0-agent 고정 - branch="$(git config -f .gitmodules "submodule.${path}.branch" || echo "${TARGET_BRANCH}")" - branch="${branch:-${TARGET_BRANCH}}" - if [[ "${branch}" != "${TARGET_BRANCH}" ]]; then - warn "${path} — .gitmodules branch=${branch} → ${TARGET_BRANCH} 로 강제" - branch="${TARGET_BRANCH}" - fi - - log ">>> ${path}" - log " url=${url} branch=${branch}" - - if ! submodule_is_cloned "${path}"; then - if $DRY_RUN; then - log " [dry-run] submodule clone 필요 — ./git_fetch_all.sh --init" - return 0 - fi - if $INIT; then - clone_submodule_repo "${path}" "${url}" "${branch}" || return 1 - else - warn "${path} — submodule 미초기화. --init 옵션 사용" - return 1 - fi - fi - - ( - cd "${ROOT_DIR}/${path}" - unset GIT_DIR GIT_WORK_TREE - - old="$(git rev-parse HEAD 2>/dev/null || echo none)" - - if $DRY_RUN; then - ensure_branch "${branch}" || true - git fetch origin "${branch}" 2>/dev/null || git fetch origin - new="$(git rev-parse "origin/${branch}" 2>/dev/null || echo unknown)" - if [[ "${old}" == "${new}" ]]; then - log " [dry-run] 원격 최신: ${old:0:7}" - else - log " [dry-run] 원격 변경 ${old:0:7} → ${new:0:7}" - git log --oneline "HEAD..origin/${branch}" 2>/dev/null | sed 's/^/ /' || true - fi - if repo_is_dirty; then - log " [dry-run] 로컬 미커밋 변경 있음 → commit/push 대상" - git status -sb | sed 's/^/ /' - else - log " [dry-run] 로컬 미커밋 변경 없음" - fi - exit 0 - fi - - # 1) 원격 fetch + 브랜치 고정 - git fetch origin "${branch}" - ensure_branch "${branch}" || exit 1 - git branch --set-upstream-to="origin/${branch}" "${branch}" 2>/dev/null || true - - # 2) 로컬 미커밋 변경이 pull 을 막으면 stash - if repo_is_dirty; then - log " 로컬 변경 stash 후 원격 반영..." - git stash push -u -m "git_fetch_all auto-stash $(date '+%Y-%m-%d %H:%M:%S')" || true - stashed=1 - fi - - # 3) GitHub 변경사항 merge (ff → rebase → merge) - if [[ "$(git rev-parse HEAD)" != "$(git rev-parse "origin/${branch}")" ]]; then - log " 원격 변경 merge: origin/${branch}" - if git merge-base --is-ancestor HEAD "origin/${branch}" 2>/dev/null; then - git merge --ff-only "origin/${branch}" - else - if ! git pull --rebase origin "${branch}"; then - warn "${path} — rebase 실패, merge 로 재시도" - git rebase --abort 2>/dev/null || true - if ! git pull --no-rebase origin "${branch}"; then - warn "${path} — merge 충돌. 수동 해결 필요" - git status -sb | sed 's/^/ /' - if [[ "${stashed}" -eq 1 ]]; then - git stash pop || true - fi - exit 1 - fi - fi - fi - ok "${path} 원격 반영 $(git log -1 --oneline)" - else - log " 원격과 동일 (fetch 완료)" - fi - - # 4) stash 복원 - if [[ "${stashed}" -eq 1 ]]; then - log " stash 복원..." - if ! git stash pop; then - warn "${path} — stash pop 충돌. 수동 해결 후 commit/push" - git status -sb | sed 's/^/ /' - exit 1 - fi - fi - - # 5) 로컬 수정사항 commit (exdev 주소는 복원 후·검증 통과 시에만) - if $DO_COMMIT && repo_is_dirty; then - log " 로컬 변경 commit..." - # push 전에 운영 WAS/RAG/Mongo 기본 주소 복원 (exdev 주소는 start-all-server.sh ENV 전용) - restore_production_endpoints_before_commit "${path}" - if ! assert_no_exdev_endpoints_for_push "${path}"; then - exit 1 - fi - log " 운영 엔드포인트 기본값 유지: WAS ${PROD_WAS_API} / Mongo ${PROD_WAS_HOST}" - git add -A - # .env 등 ignore 대상이 스테이징되지 않도록 한 번 더 정리 - git reset HEAD -- .env .env.local 2>/dev/null || true - if git diff --cached --quiet; then - log " staged 변경 없음 (ignore 된 파일만 있을 수 있음)" - else - # staged diff 에 exdev 호스트가 남아 있으면 push 금지 - if git diff --cached -U0 | grep -E '^\+' | grep -qE '127\.0\.0\.1:(27017|8086|28012)|host\.docker\.internal|EXDEV_LOCAL=true'; then - warn "${path}: staged diff 에 개발서버 주소가 포함됨 — commit 중단" - git diff --cached --stat | sed 's/^/ /' - exit 1 - fi - git commit -m "$(cat </dev/null || true - if ! git diff --cached --quiet; then - git commit -m "$(cat </dev/null || echo 0)" - if [[ "${ahead}" != "0" ]]; then - log " push origin ${branch} (ahead ${ahead})..." - if git push origin "${branch}"; then - ok "${path} push 완료" - else - warn "${path} — push 실패 (대용량 파일/권한 등). 로그 확인" - exit 1 - fi - else - log " push 생략 (원격과 동일)" - fi - else - log " [--no-push] push 생략" - fi - - new="$(git rev-parse HEAD)" - log " HEAD ${old:0:7} → ${new:0:7} $(git log -1 --oneline)" - ) -} - -failures=0 -for path in "${TARGET_PATHS[@]}"; do - if ! sync_one "${path}"; then - failures=$((failures + 1)) - fi - echo "" -done - -if ! $DRY_RUN; then - restore_monorepo_overlays -fi - -log "==========================================" -log "submodule 상태:" -git submodule status 2>/dev/null || true -for path in "${TARGET_PATHS[@]}"; do - if submodule_is_cloned "${path}"; then - printf " %-36s %s\n" "${path}" "$(git -C "${ROOT_DIR}/${path}" log -1 --oneline 2>/dev/null || echo '?')" - fi -done -log "==========================================" - -if [[ ${failures} -gt 0 ]]; then - warn "실패 ${failures}개 — 로그 확인" - exit 1 -fi - -if $DRY_RUN; then - log "완료 — dry-run (변경 없음)" -elif $DO_PUSH; then - log "완료 — ${TARGET_BRANCH}: fetch/merge → commit → push" -else - log "완료 — ${TARGET_BRANCH}: fetch/merge$($DO_COMMIT && echo ' → commit' || true) (push 생략)" -fi -log "monorepo 포인터 갱신: git add *-chatbot2.0-agent && git status" +#!/usr/bin/env bash +# 4개 submodule: 로컬 변경 → 공식 GitHub chatbot2.0-agent commit + push +# +# ★ 공식 GitHub(KoreaExpressWayCorp) 반영은 이 스크립트로만 수행한다. +# start-all-server.sh / push-to-gitea.sh 는 개발서버(Gitea/exdev)만 다룬다. +# +# ./git_fetch_all.sh # fetch/merge + commit + push (공식 GitHub) +# ./git_fetch_all.sh --init # submodule 미초기화 시 clone 포함 +# ./git_fetch_all.sh --dry-run # 실행 없이 상태만 출력 +# ./git_fetch_all.sh --no-push # commit까지, push 생략 +# ./git_fetch_all.sh --no-commit # fetch/merge만 (commit·push 생략) +# +# ★ 운영(GitHub) push 전용 — 개발서버(exdev) 주소를 커밋/push 하지 않는다. +# start-all-server.sh 가 런타임 ENV 로만 exdev(127.0.0.1 Mongo / localhost Oracle TROAD / +# host.docker.internal RAG)를 적용한다. +# 이 스크립트는 commit 직전에 운영 기본값(172.16.*)을 파일에 복원한 뒤 push 한다. +# (.env / EXDEV_LOCAL / server-dev 오버레이는 GitHub 대상이 아님) +# +# 대상 (.gitmodules) — 모두 branch=chatbot2.0-agent (main 사용 안 함): +# chatbotApi-chatbot2.0-agent +# chatbotApp-chatbot2.0-agent +# exAiChatBot-chatbot2.0-agent +# kakaoChatbotSkill-chatbot2.0-agent +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# bare repo 배포 시 상위에서 GIT_DIR 이 export 되면 submodule git 이 깨짐 +MONOREPO_GIT_DIR="${MONOREPO_GIT_DIR:-${GIT_DIR:-}}" +unset GIT_DIR GIT_WORK_TREE + +INIT=false +DRY_RUN=false +DO_COMMIT=true +DO_PUSH=true +TARGET_BRANCH="chatbot2.0-agent" + +# 명시 대상 (순서 고정). .gitmodules 와 일치해야 함. +TARGET_PATHS=( + "chatbotApi-chatbot2.0-agent" + "chatbotApp-chatbot2.0-agent" + "exAiChatBot-chatbot2.0-agent" + "kakaoChatbotSkill-chatbot2.0-agent" +) + +for arg in "$@"; do + case "$arg" in + --init) INIT=true ;; + --dry-run) DRY_RUN=true ;; + --no-commit) DO_COMMIT=false; DO_PUSH=false ;; + --no-push) DO_PUSH=false ;; + -h|--help) + sed -n '2,22p' "$0" + exit 0 + ;; + *) + echo "알 수 없는 옵션: $arg (--help)" + exit 1 + ;; + esac +done + +log() { echo "[git_fetch_all] $*"; } +warn() { echo "[git_fetch_all] ⚠️ $*" >&2; } +ok() { echo "[git_fetch_all] ✅ $*"; } + +cd "${ROOT_DIR}" + +if [[ ! -f .gitmodules ]]; then + echo "ERROR: .gitmodules 없음 — submodule 미설정" + exit 1 +fi + +resolve_monorepo_git_dir() { + if [[ -n "${MONOREPO_GIT_DIR}" && -d "${MONOREPO_GIT_DIR}" ]]; then + echo "${MONOREPO_GIT_DIR}" + elif git -C "${ROOT_DIR}" rev-parse --git-dir >/dev/null 2>&1; then + git -C "${ROOT_DIR}" rev-parse --git-dir + fi +} + +submodule_pinned_sha() { + local path="$1" + local git_dir + git_dir="$(resolve_monorepo_git_dir || true)" + [[ -n "${git_dir}" ]] || return 0 + git --git-dir="${git_dir}" ls-tree HEAD "${path}" 2>/dev/null | awk '{print $3}' +} + +clone_submodule_repo() { + local path="$1" url="$2" branch="$3" + local target="${ROOT_DIR}/${path}" + local sha + + sha="$(submodule_pinned_sha "${path}" || true)" + log " clone ${url} → ${path} (branch=${branch})" + rm -rf "${target}" + if ! git clone --branch "${branch}" --single-branch "${url}" "${target}"; then + warn "${path} — clone 실패 (서버 GitHub 미접속 시 sync-submodules-rsync.sh 사용)" + return 1 + fi + if [[ -n "${sha}" ]]; then + git -C "${target}" checkout "${sha}" 2>/dev/null || true + fi +} + +restore_monorepo_overlays() { + if [[ -f "${ROOT_DIR}/local-dev/docker-compose.rag.local.yml" ]]; then + cp "${ROOT_DIR}/local-dev/docker-compose.rag.local.yml" \ + "${ROOT_DIR}/exAiChatBot-chatbot2.0-agent/docker-compose.local.yml" + log "복원: exAiChatBot/docker-compose.local.yml (monorepo overlay)" + fi + if [[ -f "${ROOT_DIR}/server-dev/docker-compose.rag.server.yml" ]]; then + cp "${ROOT_DIR}/server-dev/docker-compose.rag.server.yml" \ + "${ROOT_DIR}/exAiChatBot-chatbot2.0-agent/docker-compose.server.yml" + log "복원: exAiChatBot/docker-compose.server.yml (monorepo overlay)" + fi +} + +# GitHub push 전: exdev 런타임 주소를 커밋하지 않고 운영 기본값(172.16.180.130) 유지 +# - 런타임(exdev): start-all-server.sh ENV / .env / EXDEV_LOCAL=true → 127.0.0.1·host.docker.internal +# - 커밋/push: application.yml·env.template·chatbot_tool_client.py 기본값을 운영 IP로 고정 +PROD_WAS_HOST="172.16.180.130" +PROD_WAS_API="http://${PROD_WAS_HOST}:8086/api" +PROD_RAG_CHAT="http://${PROD_WAS_HOST}:28012/agent/chat" +PROD_MONGO_URI="mongodb://exDev:ehfhrhdtk!!@${PROD_WAS_HOST}:27017/chatbot?heartbeatFrequencyMS=30000&minPoolSize=8&maxIdleTimeMS=18000000" +# 운영 TROAD Oracle (start-all 은 SPRING_DATASOURCE_TROAD_* ENV 로 localhost 오버라이드) +PROD_TROAD_JDBC_URL="jdbc:oracle:thin:@172.16.163.76:1522/TROAD" +PROD_TROAD_USERNAME="TROAD_APP" +PROD_TROAD_PASSWORD='!ryxhdapp2025' + +restore_production_endpoints_before_commit() { + local path="$1" + local dir="${ROOT_DIR}/${path}" + + case "${path}" in + chatbotApi-chatbot2.0-agent) + local yml="${dir}/src/main/resources/application.yml" + if [[ -f "${yml}" ]]; then + # 수동/임시로 바꾼 로컬 Mongo URI 를 운영 기본값으로 복원 (start-all 은 ENV 로만 오버라이드) + # URI 의 @ 는 perl 치환부 배열 보간을 피하기 위해 ENV 로 전달 + PROD_MONGO_URI="${PROD_MONGO_URI}" perl -i -pe \ + 's|uri:\s*mongodb://\S+|uri: $ENV{PROD_MONGO_URI}|g' "${yml}" + # troad datasource 를 로컬 Oracle 로 바꿔 둔 경우 운영값 복원 + PROD_TROAD_JDBC_URL="${PROD_TROAD_JDBC_URL}" \ + PROD_TROAD_USERNAME="${PROD_TROAD_USERNAME}" \ + PROD_TROAD_PASSWORD="${PROD_TROAD_PASSWORD}" \ + perl -i -0pe ' + s|(troad:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+url:\s*)\S+|$1$ENV{PROD_TROAD_JDBC_URL}|g; + s|(troad:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+username:\s*)\S+|$1$ENV{PROD_TROAD_USERNAME}|g; + s|(troad:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+password:\s*).*|$1"$ENV{PROD_TROAD_PASSWORD}"|g; + ' "${yml}" + fi + ;; + kakaoChatbotSkill-chatbot2.0-agent) + local yml="${dir}/src/main/resources/application.yml" + if [[ -f "${yml}" ]]; then + # ${ENV:default} 형태의 default 만 운영 IP로 고정 (실행 시 ENV 로컬 오버라이드는 유지) + PROD_WAS_API="${PROD_WAS_API}" PROD_RAG_CHAT="${PROD_RAG_CHAT}" PROD_WAS_HOST="${PROD_WAS_HOST}" \ + perl -i -pe ' + s|CHATBOT_API_BASE_URL:[^}]+}|CHATBOT_API_BASE_URL:$ENV{PROD_WAS_API}/}|g; + s|RAG_AGENT_CHAT_URL:[^}]+}|RAG_AGENT_CHAT_URL:$ENV{PROD_RAG_CHAT}}|g; + s|base-url:\s*http://127\.0\.0\.1:8086/api/?|base-url: \${CHATBOT_API_BASE_URL:$ENV{PROD_WAS_API}/}|g; + s|base-url:\s*http://host\.docker\.internal:8086/api/?|base-url: \${CHATBOT_API_BASE_URL:$ENV{PROD_WAS_API}/}|g; + s|url:\s*http://127\.0\.0\.1:28012/agent/chat|url: \${RAG_AGENT_CHAT_URL:$ENV{PROD_RAG_CHAT}}|g; + s|url:\s*http://host\.docker\.internal:28012/agent/chat|url: \${RAG_AGENT_CHAT_URL:$ENV{PROD_RAG_CHAT}}|g; + ' "${yml}" + fi + ;; + exAiChatBot-chatbot2.0-agent) + if [[ -f "${dir}/env.template" ]]; then + PROD_WAS_API="${PROD_WAS_API}" perl -i -pe \ + 's|^CHATBOT_API_BASE_URL=.*|CHATBOT_API_BASE_URL=$ENV{PROD_WAS_API}|' "${dir}/env.template" + # EXDEV_LOCAL 은 커밋 템플릿에 넣지 않음 (server-dev 전용) + perl -i -ne 'print unless /^EXDEV_LOCAL=/' "${dir}/env.template" + fi + if [[ -f "${dir}/env.example" ]]; then + PROD_WAS_API="${PROD_WAS_API}" perl -i -pe \ + 's|^CHATBOT_API_BASE_URL=.*|CHATBOT_API_BASE_URL=$ENV{PROD_WAS_API}|' "${dir}/env.example" 2>/dev/null || true + perl -i -ne 'print unless /^EXDEV_LOCAL=/' "${dir}/env.example" 2>/dev/null || true + fi + if [[ -f "${dir}/scripts/agent/chatbot_tool_client.py" ]]; then + # 운영 기본값(_PROD_WAS_API / legacy getenv default)만 복원. + PROD_WAS_API="${PROD_WAS_API}" perl -i -pe ' + s|_PROD_WAS_API = "http://[^"]+"|_PROD_WAS_API = "$ENV{PROD_WAS_API}"|g; + s|os\.getenv\("TOOL_API_BASE_URL", "http://[^"]+"\)|os.getenv("TOOL_API_BASE_URL", "$ENV{PROD_WAS_API}")|g; + s|os\.getenv\("CHATBOT_API_BASE_URL", "http://[^"]+"\)|os.getenv("CHATBOT_API_BASE_URL", "$ENV{PROD_WAS_API}")|g; + ' "${dir}/scripts/agent/chatbot_tool_client.py" + fi + # monorepo overlay compose 는 push 대상에서 제외 (로컬 exdev 전용) + git -C "${dir}" checkout HEAD -- docker-compose.server.yml docker-compose.local.yml 2>/dev/null || true + # .env 는 gitignore — 스테이징 실수 방지 + git -C "${dir}" reset HEAD -- .env .env.local 2>/dev/null || true + rm -f "${dir}/.env.local" 2>/dev/null || true + ;; + esac +} + +# 복원 후에도 개발서버 전용 호스트가 커밋 대상 설정 파일에 남으면 push 중단 +assert_no_exdev_endpoints_for_push() { + local path="$1" + local dir="${ROOT_DIR}/${path}" + local hits="" + + case "${path}" in + chatbotApi-chatbot2.0-agent) + hits="$(grep -nE 'mongodb://(127\.0\.0\.1|localhost)|host\.docker\.internal|jdbc:oracle:thin:@(127\.0\.0\.1|localhost)' \ + "${dir}/src/main/resources/application.yml" 2>/dev/null || true)" + # troad 블록이 로컬 계정(OPR_MAIN)으로 바뀌었는지도 차단 + if awk ' + /^[[:space:]]*troad:[[:space:]]*$/ { in_troad=1; next } + in_troad && /^[[:space:]]*[a-zA-Z0-9_-]+:[[:space:]]*$/ && $0 !~ /^[[:space:]]*troad:/ { in_troad=0 } + in_troad && /username:[[:space:]]*OPR_MAIN/ { found=1 } + END { exit found ? 0 : 1 } + ' "${dir}/src/main/resources/application.yml" 2>/dev/null; then + hits+=$'\n'"troad.username=OPR_MAIN (exdev local Oracle — must not commit)" + fi + ;; + kakaoChatbotSkill-chatbot2.0-agent) + hits="$(grep -nE '127\.0\.0\.1:(8086|28012)|host\.docker\.internal:(8086|28012)' \ + "${dir}/src/main/resources/application.yml" 2>/dev/null || true)" + ;; + exAiChatBot-chatbot2.0-agent) + hits="$(grep -nE '^(CHATBOT_API_BASE_URL|TOOL_API_BASE_URL)=.*(127\.0\.0\.1|host\.docker\.internal)|EXDEV_LOCAL=' \ + "${dir}/env.template" "${dir}/env.example" 2>/dev/null || true)" + hits+="$(grep -nE 'getenv\(\"TOOL_API_BASE_URL\", \"http://(127\.0\.0\.1|host\.docker\.internal)' \ + "${dir}/scripts/agent/chatbot_tool_client.py" 2>/dev/null || true)" + ;; + esac + + if [[ -n "${hits}" ]]; then + warn "${path}: 개발서버(exdev) 주소가 남아 push 중단" + echo "${hits}" | sed 's/^/ /' + warn "start-all-server.sh 런타임 ENV 로만 exdev 주소를 쓰고, 소스 기본값은 운영 IP 를 유지하세요" + return 1 + fi + return 0 +} + +submodule_is_cloned() { + local path="$1" + [[ -e "${ROOT_DIR}/${path}/.git" ]] +} + +repo_is_dirty() { + ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]] +} + +ensure_branch() { + local branch="$1" + local git_dir + # submodule 은 .git 이 파일일 수 있음 → rev-parse 사용 + git_dir="$(git rev-parse --git-dir 2>/dev/null || echo .git)" + if [[ -d "${git_dir}/rebase-merge" || -d "${git_dir}/rebase-apply" ]]; then + warn "rebase 진행 중 — 수동 해결 후 재실행 (git rebase --continue / --abort)" + return 1 + fi + if [[ -f "${git_dir}/MERGE_HEAD" ]]; then + warn "merge 진행 중 — 수동 해결 후 재실행" + return 1 + fi + + local cur + cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo DETACHED)" + if [[ "${cur}" == "${branch}" ]]; then + return 0 + fi + + if git show-ref --verify --quiet "refs/heads/${branch}"; then + git checkout "${branch}" + elif git show-ref --verify --quiet "refs/remotes/origin/${branch}"; then + git checkout -B "${branch}" "origin/${branch}" + else + git checkout -B "${branch}" + fi +} + +sync_one() { + local path="$1" + local url branch old new stashed=0 ahead + + url="$(git config -f .gitmodules "submodule.${path}.url" 2>/dev/null || true)" + if [[ -z "${url}" ]]; then + warn "${path} — .gitmodules 에 없음, 건너뜀" + return 1 + fi + + # 사용자 요구: main 금지, chatbot2.0-agent 고정 + branch="$(git config -f .gitmodules "submodule.${path}.branch" || echo "${TARGET_BRANCH}")" + branch="${branch:-${TARGET_BRANCH}}" + if [[ "${branch}" != "${TARGET_BRANCH}" ]]; then + warn "${path} — .gitmodules branch=${branch} → ${TARGET_BRANCH} 로 강제" + branch="${TARGET_BRANCH}" + fi + + log ">>> ${path}" + log " url=${url} branch=${branch}" + + if ! submodule_is_cloned "${path}"; then + if $DRY_RUN; then + log " [dry-run] submodule clone 필요 — ./git_fetch_all.sh --init" + return 0 + fi + if $INIT; then + clone_submodule_repo "${path}" "${url}" "${branch}" || return 1 + else + warn "${path} — submodule 미초기화. --init 옵션 사용" + return 1 + fi + fi + + ( + cd "${ROOT_DIR}/${path}" + unset GIT_DIR GIT_WORK_TREE + + old="$(git rev-parse HEAD 2>/dev/null || echo none)" + + if $DRY_RUN; then + ensure_branch "${branch}" || true + git fetch origin "${branch}" 2>/dev/null || git fetch origin + new="$(git rev-parse "origin/${branch}" 2>/dev/null || echo unknown)" + if [[ "${old}" == "${new}" ]]; then + log " [dry-run] 원격 최신: ${old:0:7}" + else + log " [dry-run] 원격 변경 ${old:0:7} → ${new:0:7}" + git log --oneline "HEAD..origin/${branch}" 2>/dev/null | sed 's/^/ /' || true + fi + if repo_is_dirty; then + log " [dry-run] 로컬 미커밋 변경 있음 → commit/push 대상" + git status -sb | sed 's/^/ /' + else + log " [dry-run] 로컬 미커밋 변경 없음" + fi + exit 0 + fi + + # 1) 원격 fetch + 브랜치 고정 + git fetch origin "${branch}" + ensure_branch "${branch}" || exit 1 + git branch --set-upstream-to="origin/${branch}" "${branch}" 2>/dev/null || true + + # 2) 로컬 미커밋 변경이 pull 을 막으면 stash + if repo_is_dirty; then + log " 로컬 변경 stash 후 원격 반영..." + git stash push -u -m "git_fetch_all auto-stash $(date '+%Y-%m-%d %H:%M:%S')" || true + stashed=1 + fi + + # 3) GitHub 변경사항 merge (ff → rebase → merge) + if [[ "$(git rev-parse HEAD)" != "$(git rev-parse "origin/${branch}")" ]]; then + log " 원격 변경 merge: origin/${branch}" + if git merge-base --is-ancestor HEAD "origin/${branch}" 2>/dev/null; then + git merge --ff-only "origin/${branch}" + else + if ! git pull --rebase origin "${branch}"; then + warn "${path} — rebase 실패, merge 로 재시도" + git rebase --abort 2>/dev/null || true + if ! git pull --no-rebase origin "${branch}"; then + warn "${path} — merge 충돌. 수동 해결 필요" + git status -sb | sed 's/^/ /' + if [[ "${stashed}" -eq 1 ]]; then + git stash pop || true + fi + exit 1 + fi + fi + fi + ok "${path} 원격 반영 $(git log -1 --oneline)" + else + log " 원격과 동일 (fetch 완료)" + fi + + # 4) stash 복원 + if [[ "${stashed}" -eq 1 ]]; then + log " stash 복원..." + if ! git stash pop; then + warn "${path} — stash pop 충돌. 수동 해결 후 commit/push" + git status -sb | sed 's/^/ /' + exit 1 + fi + fi + + # 5) 로컬 수정사항 commit (exdev 주소는 복원 후·검증 통과 시에만) + if $DO_COMMIT && repo_is_dirty; then + log " 로컬 변경 commit..." + # push 전에 운영 WAS/RAG/Mongo 기본 주소 복원 (exdev 주소는 start-all-server.sh ENV 전용) + restore_production_endpoints_before_commit "${path}" + if ! assert_no_exdev_endpoints_for_push "${path}"; then + exit 1 + fi + log " 운영 엔드포인트 기본값 유지: WAS ${PROD_WAS_API} / Mongo ${PROD_WAS_HOST}" + git add -A + # .env 등 ignore 대상이 스테이징되지 않도록 한 번 더 정리 + git reset HEAD -- .env .env.local 2>/dev/null || true + if git diff --cached --quiet; then + log " staged 변경 없음 (ignore 된 파일만 있을 수 있음)" + else + # staged diff 에 exdev 호스트가 남아 있으면 push 금지 + if git diff --cached -U0 | grep -E '^\+' | grep -qE '127\.0\.0\.1:(27017|8086|28012)|host\.docker\.internal|EXDEV_LOCAL=true'; then + warn "${path}: staged diff 에 개발서버 주소가 포함됨 — commit 중단" + git diff --cached --stat | sed 's/^/ /' + exit 1 + fi + git commit -m "$(cat </dev/null || true + if ! git diff --cached --quiet; then + git commit -m "$(cat </dev/null || echo 0)" + if [[ "${ahead}" != "0" ]]; then + log " push origin ${branch} (ahead ${ahead})..." + if git push origin "${branch}"; then + ok "${path} push 완료" + else + warn "${path} — push 실패 (대용량 파일/권한 등). 로그 확인" + exit 1 + fi + else + log " push 생략 (원격과 동일)" + fi + else + log " [--no-push] push 생략" + fi + + new="$(git rev-parse HEAD)" + log " HEAD ${old:0:7} → ${new:0:7} $(git log -1 --oneline)" + ) +} + +failures=0 +for path in "${TARGET_PATHS[@]}"; do + if ! sync_one "${path}"; then + failures=$((failures + 1)) + fi + echo "" +done + +if ! $DRY_RUN; then + restore_monorepo_overlays +fi + +log "==========================================" +log "submodule 상태:" +git submodule status 2>/dev/null || true +for path in "${TARGET_PATHS[@]}"; do + if submodule_is_cloned "${path}"; then + printf " %-36s %s\n" "${path}" "$(git -C "${ROOT_DIR}/${path}" log -1 --oneline 2>/dev/null || echo '?')" + fi +done +log "==========================================" + +if [[ ${failures} -gt 0 ]]; then + warn "실패 ${failures}개 — 로그 확인" + exit 1 +fi + +if $DRY_RUN; then + log "완료 — dry-run (변경 없음)" +elif $DO_PUSH; then + log "완료 — ${TARGET_BRANCH}: fetch/merge → commit → push" +else + log "완료 — ${TARGET_BRANCH}: fetch/merge$($DO_COMMIT && echo ' → commit' || true) (push 생략)" +fi +log "monorepo 포인터 갱신: git add *-chatbot2.0-agent && git status" diff --git a/local-dev/bin/rsync b/local-dev/bin/rsync index 59b082d..e06c35d 100755 --- a/local-dev/bin/rsync +++ b/local-dev/bin/rsync @@ -1,124 +1,124 @@ -#!/usr/bin/env bash -# Minimal rsync stand-in for Windows Git Bash (tar-over-ssh). -# Supports the flags used by server-dev/sync-submodules-rsync.sh: -# rsync -az [--delete] [--exclude PATTERN ...] SRC/ HOST:DEST/ -set -euo pipefail - -DELETE=false -EXCLUDES=() -ARGS=() - -while (($#)); do - case "$1" in - -az|-a|-z|-v|-q|-P|--progress) shift ;; - --delete) DELETE=true; shift ;; - --exclude) - EXCLUDES+=("$2"); shift 2 ;; - --exclude=*) - EXCLUDES+=("${1#--exclude=}"); shift ;; - -*) - # ignore unknown short/long flags used by callers - shift ;; - *) - ARGS+=("$1"); shift ;; - esac -done - -if ((${#ARGS[@]} < 2)); then - echo "rsync-shim: need SRC and DEST" >&2 - exit 1 -fi - -SRC="${ARGS[0]}" -DEST="${ARGS[1]}" - -# Git Bash /d/foo → Windows D:/foo (OpenSSH scp 용) -to_win_path() { - local p="$1" - if [[ "${p}" =~ ^/([a-zA-Z])/(.*)$ ]]; then - local drive rest - drive="$(echo "${BASH_REMATCH[1]}" | tr 'a-z' 'A-Z')" - rest="${BASH_REMATCH[2]}" - echo "${drive}:/${rest}" - else - echo "${p}" - fi -} - -copy_file_remote() { - local src_file="$1" - local dest="$2" - local win_src - win_src="$(to_win_path "${src_file}")" - if [[ -f "${win_src}" || -f "${src_file}" ]]; then - local local_src="${src_file}" - [[ -f "${win_src}" ]] && local_src="${win_src}" - scp -o BatchMode=yes -q "${local_src}" "${dest}" && return 0 - fi - # fallback: tar stream single file - local base dir - base="$(basename "${src_file}")" - dir="$(cd "$(dirname "${src_file}")" && pwd)" - local host="${dest%%:*}" - local remote="${dest#*:}" - local remote_dir remote_base - if [[ "${remote}" == */ ]]; then - remote_dir="${remote%/}" - remote_base="${base}" - else - remote_dir="$(dirname "${remote}")" - remote_base="$(basename "${remote}")" - fi - ssh -o BatchMode=yes "${host}" "mkdir -p '${remote_dir}'" - tar -C "${dir}" -cf - "${base}" \ - | ssh -o BatchMode=yes "${host}" "tar -C '${remote_dir}' -xf - && if [[ '${remote_base}' != '${base}' ]]; then mv '${remote_dir}/${base}' '${remote_dir}/${remote_base}'; fi" -} - -# file → file copy (overlay yml etc.) -if [[ -f "${SRC}" && "${DEST}" == *:* ]]; then - copy_file_remote "${SRC}" "${DEST}" - exit 0 -fi - -# normalize trailing slash semantics like rsync SRC/ -SRC_DIR="${SRC%/}" -if [[ ! -d "${SRC_DIR}" ]]; then - # allow copying a single file into a remote dir - if [[ -f "${SRC_DIR}" ]]; then - copy_file_remote "${SRC_DIR}" "${DEST}" - exit 0 - fi - echo "rsync-shim: source not found: ${SRC}" >&2 - exit 1 -fi - -if [[ "${DEST}" != *:* ]]; then - echo "rsync-shim: only remote DEST (host:path) supported" >&2 - exit 1 -fi - -HOST="${DEST%%:*}" -REMOTE_PATH="${DEST#*:}" -REMOTE_PATH="${REMOTE_PATH%/}" - -TAR_EXCLUDES=() -for pat in "${EXCLUDES[@]}"; do - TAR_EXCLUDES+=(--exclude "${pat}") -done - -ssh -o BatchMode=yes "${HOST}" "mkdir -p '${REMOTE_PATH}'" - -if $DELETE; then - # wipe remote dir contents then unpack (approx --delete) - # 서버 public-* 등이 root/docker 소유일 수 있어 실패해도 이어서 덮어쓴다 - ssh -o BatchMode=yes "${HOST}" \ - "chmod -R u+w '${REMOTE_PATH}' 2>/dev/null || true; \ - find '${REMOTE_PATH}' -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true" \ - || true -fi - -# shellcheck disable=SC2086 -tar -C "${SRC_DIR}" "${TAR_EXCLUDES[@]}" -cf - . \ - | ssh -o BatchMode=yes "${HOST}" "tar -C '${REMOTE_PATH}' -xf -" - -echo "rsync-shim: synced ${SRC_DIR}/ -> ${HOST}:${REMOTE_PATH}/" +#!/usr/bin/env bash +# Minimal rsync stand-in for Windows Git Bash (tar-over-ssh). +# Supports the flags used by server-dev/sync-submodules-rsync.sh: +# rsync -az [--delete] [--exclude PATTERN ...] SRC/ HOST:DEST/ +set -euo pipefail + +DELETE=false +EXCLUDES=() +ARGS=() + +while (($#)); do + case "$1" in + -az|-a|-z|-v|-q|-P|--progress) shift ;; + --delete) DELETE=true; shift ;; + --exclude) + EXCLUDES+=("$2"); shift 2 ;; + --exclude=*) + EXCLUDES+=("${1#--exclude=}"); shift ;; + -*) + # ignore unknown short/long flags used by callers + shift ;; + *) + ARGS+=("$1"); shift ;; + esac +done + +if ((${#ARGS[@]} < 2)); then + echo "rsync-shim: need SRC and DEST" >&2 + exit 1 +fi + +SRC="${ARGS[0]}" +DEST="${ARGS[1]}" + +# Git Bash /d/foo → Windows D:/foo (OpenSSH scp 용) +to_win_path() { + local p="$1" + if [[ "${p}" =~ ^/([a-zA-Z])/(.*)$ ]]; then + local drive rest + drive="$(echo "${BASH_REMATCH[1]}" | tr 'a-z' 'A-Z')" + rest="${BASH_REMATCH[2]}" + echo "${drive}:/${rest}" + else + echo "${p}" + fi +} + +copy_file_remote() { + local src_file="$1" + local dest="$2" + local win_src + win_src="$(to_win_path "${src_file}")" + if [[ -f "${win_src}" || -f "${src_file}" ]]; then + local local_src="${src_file}" + [[ -f "${win_src}" ]] && local_src="${win_src}" + scp -o BatchMode=yes -q "${local_src}" "${dest}" && return 0 + fi + # fallback: tar stream single file + local base dir + base="$(basename "${src_file}")" + dir="$(cd "$(dirname "${src_file}")" && pwd)" + local host="${dest%%:*}" + local remote="${dest#*:}" + local remote_dir remote_base + if [[ "${remote}" == */ ]]; then + remote_dir="${remote%/}" + remote_base="${base}" + else + remote_dir="$(dirname "${remote}")" + remote_base="$(basename "${remote}")" + fi + ssh -o BatchMode=yes "${host}" "mkdir -p '${remote_dir}'" + tar -C "${dir}" -cf - "${base}" \ + | ssh -o BatchMode=yes "${host}" "tar -C '${remote_dir}' -xf - && if [[ '${remote_base}' != '${base}' ]]; then mv '${remote_dir}/${base}' '${remote_dir}/${remote_base}'; fi" +} + +# file → file copy (overlay yml etc.) +if [[ -f "${SRC}" && "${DEST}" == *:* ]]; then + copy_file_remote "${SRC}" "${DEST}" + exit 0 +fi + +# normalize trailing slash semantics like rsync SRC/ +SRC_DIR="${SRC%/}" +if [[ ! -d "${SRC_DIR}" ]]; then + # allow copying a single file into a remote dir + if [[ -f "${SRC_DIR}" ]]; then + copy_file_remote "${SRC_DIR}" "${DEST}" + exit 0 + fi + echo "rsync-shim: source not found: ${SRC}" >&2 + exit 1 +fi + +if [[ "${DEST}" != *:* ]]; then + echo "rsync-shim: only remote DEST (host:path) supported" >&2 + exit 1 +fi + +HOST="${DEST%%:*}" +REMOTE_PATH="${DEST#*:}" +REMOTE_PATH="${REMOTE_PATH%/}" + +TAR_EXCLUDES=() +for pat in "${EXCLUDES[@]}"; do + TAR_EXCLUDES+=(--exclude "${pat}") +done + +ssh -o BatchMode=yes "${HOST}" "mkdir -p '${REMOTE_PATH}'" + +if $DELETE; then + # wipe remote dir contents then unpack (approx --delete) + # 서버 public-* 등이 root/docker 소유일 수 있어 실패해도 이어서 덮어쓴다 + ssh -o BatchMode=yes "${HOST}" \ + "chmod -R u+w '${REMOTE_PATH}' 2>/dev/null || true; \ + find '${REMOTE_PATH}' -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true" \ + || true +fi + +# shellcheck disable=SC2086 +tar -C "${SRC_DIR}" "${TAR_EXCLUDES[@]}" -cf - . \ + | ssh -o BatchMode=yes "${HOST}" "tar -C '${REMOTE_PATH}' -xf -" + +echo "rsync-shim: synced ${SRC_DIR}/ -> ${HOST}:${REMOTE_PATH}/" diff --git a/server-dev/deploy.sh b/server-dev/deploy.sh index cf3287a..19e4398 100755 --- a/server-dev/deploy.sh +++ b/server-dev/deploy.sh @@ -1,154 +1,154 @@ -#!/usr/bin/env bash -# ============================================================================== -# exAichatbot_agent — Gitea push → exdev 서버 빌드·배포 -# 설치: /Volumes/ADATA/git/deploy-exAichatbot_agent.sh -# 호출: post-receive (main 브랜치 push) -# ============================================================================== -set -euo pipefail - -GIT_DIR="${GIT_DIR:-/Volumes/ADATA/gitea/data/gitea-repositories/aidev/exaichatbot_agent.git}" -WORK_TREE="${WORK_TREE:-/Users/aidev/dev/exAichatbot_agent}" -BRANCH="${BRANCH:-main}" -DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/exAichatbot_agent.log}" -LOG_TAG="[exAichatbot_agent-deploy]" - -export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/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 $*"; } -fail() { log "[ERROR] $*"; exit 1; } - -LOCK_FILE="/tmp/exAichatbot_agent_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 - fail "다른 배포 진행 중 (PID: $LOCK_PID)" - fi - rm -f "$LOCK_FILE" -fi -echo $$ > "$LOCK_FILE" -DEPLOY_OK=false -trap 'rm -f "$LOCK_FILE"; if ! $DEPLOY_OK; then log "[ERROR] 배포 실패"; fi; log "락 해제"' EXIT - -for arg in "$@"; do - case "$arg" in - --start-only) START_ONLY=true ;; - esac -done -START_ONLY="${START_ONLY:-false}" - -log "======================================================" -log " exAichatbot Agent 2.0 배포 시작" -log " GIT_DIR : $GIT_DIR" -log " WORK_TREE : $WORK_TREE" -log " BRANCH : $BRANCH" -log " START_ONLY: $START_ONLY" -log "======================================================" - -[[ -d "$GIT_DIR" ]] || fail "GIT_DIR 없음: $GIT_DIR" -mkdir -p "$WORK_TREE" - -chmod +x "$WORK_TREE/server-dev/"*.sh 2>/dev/null || true -chmod +x "$WORK_TREE/git_fetch_all.sh" 2>/dev/null || true -chmod +x "$WORK_TREE/chatbotApp-chatbot2.0-agent/build.sh" 2>/dev/null || true - -sync_worktree_submodules() { - local tree="$1" - local path ready=0 - for path in chatbotApp-chatbot2.0-agent chatbotApi-chatbot2.0-agent \ - exAiChatBot-chatbot2.0-agent kakaoChatbotSkill-chatbot2.0-agent; do - if [[ -f "${tree}/${path}/build.gradle" ]] || [[ -f "${tree}/${path}/build.sh" ]] \ - || [[ -f "${tree}/${path}/docker/service-slim.Dockerfile" ]]; then - ready=$((ready + 1)) - fi - done - if [[ ${ready} -ge 4 ]]; then - log "submodule 소스 이미 존재 (Gitea/rsync) — 공식 GitHub fetch 생략" - if [[ -f "${tree}/server-dev/docker-compose.rag.server.yml" ]]; then - cp "${tree}/server-dev/docker-compose.rag.server.yml" \ - "${tree}/exAiChatBot-chatbot2.0-agent/docker-compose.server.yml" 2>/dev/null || true - fi - return 0 - fi - # 공식 GitHub 는 deploy 경로에서 절대 동기화/push 하지 않음. - # 소스가 비어 있으면 로컬 Mac 의 rsync (sync-submodules-rsync.sh) 가 필요. - log "[WARN] submodule 소스 부족 (${ready}/4) — 공식 GitHub fetch 안 함. 로컬 rsync 필요" -} - -resolve_java17_home() { - local home="" candidate - for candidate in \ - "/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home" \ - "/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"; do - if [[ -x "${candidate}/bin/java" ]]; then - home="${candidate}" - break - fi - done - if [[ -z "${home}" ]] && /usr/libexec/java_home -v 17 >/dev/null 2>&1; then - home="$(/usr/libexec/java_home -v 17)" - fi - [[ -n "${home}" ]] || fail "Java 17 필요 — brew install openjdk@17" - echo "${home}" -} - -# 서비스 중지 전에 Java 컴파일만 검증 — 실패 시 가동 중인 스택을 유지한다. -# Gitea post-receive / 비로그인 SSH 는 PATH 에 brew java 가 없고 /usr/bin/java 는 stub 이므로 -# JAVA_HOME 을 반드시 export 해야 gradlew 부트스트랩이 성공한다. -preflight_java_compile() { - local java_home dir - java_home="$(resolve_java17_home)" - export JAVA_HOME="${java_home}" - export PATH="${JAVA_HOME}/bin:/opt/homebrew/bin:/opt/homebrew/sbin:${PATH}" - log "[Phase 0] Java 사전 컴파일 검증 (Java 17: ${JAVA_HOME})" - for dir in \ - "${WORK_TREE}/kakaoChatbotSkill-chatbot2.0-agent" \ - "${WORK_TREE}/chatbotApi-chatbot2.0-agent"; do - [[ -x "${dir}/gradlew" ]] || fail "gradlew 없음: ${dir}" - log " compileJava: $(basename "${dir}")" - if ! ( - cd "${dir}" - export JAVA_HOME - export PATH="${JAVA_HOME}/bin:${PATH}" - ./gradlew -Dorg.gradle.java.home="${JAVA_HOME}" compileJava --quiet - ); then - fail "사전 컴파일 실패: $(basename "${dir}") — 기존 서비스는 그대로 유지됨" - fi - done - log " ✅ Java 사전 컴파일 OK" -} - -if ! $START_ONLY; then - log "[Phase 2] checkout (Gitea work tree) — 공식 GitHub 미사용" - git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f "$BRANCH" - rm -f "$WORK_TREE/.git" - sync_worktree_submodules "$WORK_TREE" -fi - -preflight_java_compile - -log "[Phase 1] stop-all-server.sh" -"$WORK_TREE/server-dev/stop-all-server.sh" --keep-caddy || true - -log "[Phase 3] .env 준비" -ENV_FILE="$WORK_TREE/exAiChatBot-chatbot2.0-agent/.env" -if [[ ! -f "$ENV_FILE" ]]; then - cp "$WORK_TREE/server-dev/env.exdev-server.template" "$ENV_FILE" - log ".env 생성 (env.exdev-server.template)" -fi - -log "[Phase 4] start-all-server.sh (기동)" -START_ARGS=(--skip-legacy-stop --no-pull --no-push) -if $START_ONLY || [[ -f "$WORK_TREE/chatbotApp-chatbot2.0-agent/public/index.html" ]]; then - START_ARGS+=(--no-build) -fi -"$WORK_TREE/server-dev/start-all-server.sh" "${START_ARGS[@]}" - -COMMIT="$(git --git-dir="$GIT_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown)" -DEPLOY_OK=true -log "======================================================" -log " 배포 완료 — $COMMIT" -log " UI: https://chatbot.exdev.co.kr/chatbot/ai/" -log "======================================================" +#!/usr/bin/env bash +# ============================================================================== +# exAichatbot_agent — Gitea push → exdev 서버 빌드·배포 +# 설치: /Volumes/ADATA/git/deploy-exAichatbot_agent.sh +# 호출: post-receive (main 브랜치 push) +# ============================================================================== +set -euo pipefail + +GIT_DIR="${GIT_DIR:-/Volumes/ADATA/gitea/data/gitea-repositories/aidev/exaichatbot_agent.git}" +WORK_TREE="${WORK_TREE:-/Users/aidev/dev/exAichatbot_agent}" +BRANCH="${BRANCH:-main}" +DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/exAichatbot_agent.log}" +LOG_TAG="[exAichatbot_agent-deploy]" + +export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/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 $*"; } +fail() { log "[ERROR] $*"; exit 1; } + +LOCK_FILE="/tmp/exAichatbot_agent_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 + fail "다른 배포 진행 중 (PID: $LOCK_PID)" + fi + rm -f "$LOCK_FILE" +fi +echo $$ > "$LOCK_FILE" +DEPLOY_OK=false +trap 'rm -f "$LOCK_FILE"; if ! $DEPLOY_OK; then log "[ERROR] 배포 실패"; fi; log "락 해제"' EXIT + +for arg in "$@"; do + case "$arg" in + --start-only) START_ONLY=true ;; + esac +done +START_ONLY="${START_ONLY:-false}" + +log "======================================================" +log " exAichatbot Agent 2.0 배포 시작" +log " GIT_DIR : $GIT_DIR" +log " WORK_TREE : $WORK_TREE" +log " BRANCH : $BRANCH" +log " START_ONLY: $START_ONLY" +log "======================================================" + +[[ -d "$GIT_DIR" ]] || fail "GIT_DIR 없음: $GIT_DIR" +mkdir -p "$WORK_TREE" + +chmod +x "$WORK_TREE/server-dev/"*.sh 2>/dev/null || true +chmod +x "$WORK_TREE/git_fetch_all.sh" 2>/dev/null || true +chmod +x "$WORK_TREE/chatbotApp-chatbot2.0-agent/build.sh" 2>/dev/null || true + +sync_worktree_submodules() { + local tree="$1" + local path ready=0 + for path in chatbotApp-chatbot2.0-agent chatbotApi-chatbot2.0-agent \ + exAiChatBot-chatbot2.0-agent kakaoChatbotSkill-chatbot2.0-agent; do + if [[ -f "${tree}/${path}/build.gradle" ]] || [[ -f "${tree}/${path}/build.sh" ]] \ + || [[ -f "${tree}/${path}/docker/service-slim.Dockerfile" ]]; then + ready=$((ready + 1)) + fi + done + if [[ ${ready} -ge 4 ]]; then + log "submodule 소스 이미 존재 (Gitea/rsync) — 공식 GitHub fetch 생략" + if [[ -f "${tree}/server-dev/docker-compose.rag.server.yml" ]]; then + cp "${tree}/server-dev/docker-compose.rag.server.yml" \ + "${tree}/exAiChatBot-chatbot2.0-agent/docker-compose.server.yml" 2>/dev/null || true + fi + return 0 + fi + # 공식 GitHub 는 deploy 경로에서 절대 동기화/push 하지 않음. + # 소스가 비어 있으면 로컬 Mac 의 rsync (sync-submodules-rsync.sh) 가 필요. + log "[WARN] submodule 소스 부족 (${ready}/4) — 공식 GitHub fetch 안 함. 로컬 rsync 필요" +} + +resolve_java17_home() { + local home="" candidate + for candidate in \ + "/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home" \ + "/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"; do + if [[ -x "${candidate}/bin/java" ]]; then + home="${candidate}" + break + fi + done + if [[ -z "${home}" ]] && /usr/libexec/java_home -v 17 >/dev/null 2>&1; then + home="$(/usr/libexec/java_home -v 17)" + fi + [[ -n "${home}" ]] || fail "Java 17 필요 — brew install openjdk@17" + echo "${home}" +} + +# 서비스 중지 전에 Java 컴파일만 검증 — 실패 시 가동 중인 스택을 유지한다. +# Gitea post-receive / 비로그인 SSH 는 PATH 에 brew java 가 없고 /usr/bin/java 는 stub 이므로 +# JAVA_HOME 을 반드시 export 해야 gradlew 부트스트랩이 성공한다. +preflight_java_compile() { + local java_home dir + java_home="$(resolve_java17_home)" + export JAVA_HOME="${java_home}" + export PATH="${JAVA_HOME}/bin:/opt/homebrew/bin:/opt/homebrew/sbin:${PATH}" + log "[Phase 0] Java 사전 컴파일 검증 (Java 17: ${JAVA_HOME})" + for dir in \ + "${WORK_TREE}/kakaoChatbotSkill-chatbot2.0-agent" \ + "${WORK_TREE}/chatbotApi-chatbot2.0-agent"; do + [[ -x "${dir}/gradlew" ]] || fail "gradlew 없음: ${dir}" + log " compileJava: $(basename "${dir}")" + if ! ( + cd "${dir}" + export JAVA_HOME + export PATH="${JAVA_HOME}/bin:${PATH}" + ./gradlew -Dorg.gradle.java.home="${JAVA_HOME}" compileJava --quiet + ); then + fail "사전 컴파일 실패: $(basename "${dir}") — 기존 서비스는 그대로 유지됨" + fi + done + log " ✅ Java 사전 컴파일 OK" +} + +if ! $START_ONLY; then + log "[Phase 2] checkout (Gitea work tree) — 공식 GitHub 미사용" + git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f "$BRANCH" + rm -f "$WORK_TREE/.git" + sync_worktree_submodules "$WORK_TREE" +fi + +preflight_java_compile + +log "[Phase 1] stop-all-server.sh" +"$WORK_TREE/server-dev/stop-all-server.sh" --keep-caddy || true + +log "[Phase 3] .env 준비" +ENV_FILE="$WORK_TREE/exAiChatBot-chatbot2.0-agent/.env" +if [[ ! -f "$ENV_FILE" ]]; then + cp "$WORK_TREE/server-dev/env.exdev-server.template" "$ENV_FILE" + log ".env 생성 (env.exdev-server.template)" +fi + +log "[Phase 4] start-all-server.sh (기동)" +START_ARGS=(--skip-legacy-stop --no-pull --no-push) +if $START_ONLY || [[ -f "$WORK_TREE/chatbotApp-chatbot2.0-agent/public/index.html" ]]; then + START_ARGS+=(--no-build) +fi +"$WORK_TREE/server-dev/start-all-server.sh" "${START_ARGS[@]}" + +COMMIT="$(git --git-dir="$GIT_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown)" +DEPLOY_OK=true +log "======================================================" +log " 배포 완료 — $COMMIT" +log " UI: https://chatbot.exdev.co.kr/chatbot/ai/" +log "======================================================" diff --git a/server-dev/wait-remote-deploy.sh b/server-dev/wait-remote-deploy.sh index 61ca8a0..b35ba98 100755 --- a/server-dev/wait-remote-deploy.sh +++ b/server-dev/wait-remote-deploy.sh @@ -1,86 +1,86 @@ -#!/usr/bin/env bash -# Gitea push 후 chatbot.exdev.co.kr 배포 완료 대기 -# - UI 응답 또는 배포 로그의 "배포 완료" 확인 -# - "배포 실패"/컴파일 오류는 즉시 종료 (900s 무의미 대기 방지) -set -euo pipefail - -EXDEV_SSH="${EXDEV_SSH:-aidev@chatbot.exdev.co.kr}" -DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/exAichatbot_agent.log}" -PUBLIC_URL="${PUBLIC_BASE_URL:-https://chatbot.exdev.co.kr}/chatbot/ai/" -MAX_WAIT="${DEPLOY_WAIT_SEC:-900}" - -log() { echo "[wait-deploy] $*"; } - -ssh_cmd() { - ssh -o BatchMode=yes -o ConnectTimeout=15 "${EXDEV_SSH}" "$@" -} - -if ! ssh_cmd "test -f '${DEPLOY_LOG}'" 2>/dev/null; then - log "배포 로그 없음 — SSH로 deploy 스크립트 직접 실행" - ssh_cmd "bash /Volumes/ADATA/git/deploy-exAichatbot_agent.sh" || true -fi - -# 대기 시작 시점의 로그 길이 — 이후 새로 찍힌 줄만 성공/실패 판정에 사용 -LOG_BASE_BYTES="$(ssh_cmd "wc -c < '${DEPLOY_LOG}' 2>/dev/null || echo 0" | tr -d '[:space:]')" -LOG_BASE_BYTES="${LOG_BASE_BYTES:-0}" - -log "배포 완료 대기 (최대 ${MAX_WAIT}s) — ${EXDEV_SSH}" -start_ts="$(date +%s)" -deadline=$((start_ts + MAX_WAIT)) - -new_log_tail() { - ssh_cmd "tail -c +$((LOG_BASE_BYTES + 1)) '${DEPLOY_LOG}' 2>/dev/null | tail -80" || true -} - -while (( $(date +%s) < deadline )); do - recent="$(new_log_tail)" - - # 신규 로그에 실패 마커가 있으면 즉시 종료 - if echo "${recent}" | grep -qE '\[ERROR\] 배포 실패|ERROR: chatbotApi 컴파일 실패|BUILD FAILED|\[ERROR\] 다른 배포 진행 중'; then - log "⚠️ 원격 배포 실패 감지 — 최근 로그:" - echo "${recent}" | tail -40 - exit 1 - fi - - if curl -sf --max-time 5 "${PUBLIC_URL}" 2>/dev/null | grep -q 'data-chatbot-app'; then - # UI 살아있어도, 이번 배포가 아직 실패로 끝났으면 성공으로 치지 않음 - if echo "${recent}" | grep -qE '\[ERROR\] 배포 실패|ERROR: chatbotApi 컴파일 실패|BUILD FAILED'; then - log "⚠️ UI는 응답하나 이번 배포 로그에 실패가 있습니다" - echo "${recent}" | tail -40 - exit 1 - fi - if echo "${recent}" | grep -q '배포 완료'; then - log "✅ UI 응답 + 배포 완료 로그: ${PUBLIC_URL}" - ssh_cmd "tail -8 '${DEPLOY_LOG}'" 2>/dev/null || true - exit 0 - fi - # start-only 직후 UI 가 이전 빌드로 살아 있을 수 있음 — 신규 성공 로그를 우선 - if echo "${recent}" | grep -q '배포 시작'; then - : - else - log "✅ UI 응답: ${PUBLIC_URL}" - ssh_cmd "tail -8 '${DEPLOY_LOG}'" 2>/dev/null || true - exit 0 - fi - fi - - if echo "${recent}" | grep -q '배포 완료'; then - last_line="$(ssh_cmd "tail -1 '${DEPLOY_LOG}'" 2>/dev/null || true)" - if [[ "${last_line}" == *"배포 완료"* ]] || [[ "${last_line}" == *"락 해제"* ]]; then - # 성공 후 trap 이 락 해제를 찍을 수 있음 - if echo "${recent}" | grep -qE '\[ERROR\] 배포 실패|BUILD FAILED'; then - log "⚠️ 배포 실패 로그 감지" - echo "${recent}" | tail -40 - exit 1 - fi - log "✅ 배포 로그 확인" - ssh_cmd "tail -8 '${DEPLOY_LOG}'" 2>/dev/null || true - exit 0 - fi - fi - sleep 5 -done - -log "⚠️ 배포 대기 타임아웃 — 로그 확인: ssh ${EXDEV_SSH} tail -50 ${DEPLOY_LOG}" -new_log_tail | tail -40 -exit 1 +#!/usr/bin/env bash +# Gitea push 후 chatbot.exdev.co.kr 배포 완료 대기 +# - UI 응답 또는 배포 로그의 "배포 완료" 확인 +# - "배포 실패"/컴파일 오류는 즉시 종료 (900s 무의미 대기 방지) +set -euo pipefail + +EXDEV_SSH="${EXDEV_SSH:-aidev@chatbot.exdev.co.kr}" +DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/exAichatbot_agent.log}" +PUBLIC_URL="${PUBLIC_BASE_URL:-https://chatbot.exdev.co.kr}/chatbot/ai/" +MAX_WAIT="${DEPLOY_WAIT_SEC:-900}" + +log() { echo "[wait-deploy] $*"; } + +ssh_cmd() { + ssh -o BatchMode=yes -o ConnectTimeout=15 "${EXDEV_SSH}" "$@" +} + +if ! ssh_cmd "test -f '${DEPLOY_LOG}'" 2>/dev/null; then + log "배포 로그 없음 — SSH로 deploy 스크립트 직접 실행" + ssh_cmd "bash /Volumes/ADATA/git/deploy-exAichatbot_agent.sh" || true +fi + +# 대기 시작 시점의 로그 길이 — 이후 새로 찍힌 줄만 성공/실패 판정에 사용 +LOG_BASE_BYTES="$(ssh_cmd "wc -c < '${DEPLOY_LOG}' 2>/dev/null || echo 0" | tr -d '[:space:]')" +LOG_BASE_BYTES="${LOG_BASE_BYTES:-0}" + +log "배포 완료 대기 (최대 ${MAX_WAIT}s) — ${EXDEV_SSH}" +start_ts="$(date +%s)" +deadline=$((start_ts + MAX_WAIT)) + +new_log_tail() { + ssh_cmd "tail -c +$((LOG_BASE_BYTES + 1)) '${DEPLOY_LOG}' 2>/dev/null | tail -80" || true +} + +while (( $(date +%s) < deadline )); do + recent="$(new_log_tail)" + + # 신규 로그에 실패 마커가 있으면 즉시 종료 + if echo "${recent}" | grep -qE '\[ERROR\] 배포 실패|ERROR: chatbotApi 컴파일 실패|BUILD FAILED|\[ERROR\] 다른 배포 진행 중'; then + log "⚠️ 원격 배포 실패 감지 — 최근 로그:" + echo "${recent}" | tail -40 + exit 1 + fi + + if curl -sf --max-time 5 "${PUBLIC_URL}" 2>/dev/null | grep -q 'data-chatbot-app'; then + # UI 살아있어도, 이번 배포가 아직 실패로 끝났으면 성공으로 치지 않음 + if echo "${recent}" | grep -qE '\[ERROR\] 배포 실패|ERROR: chatbotApi 컴파일 실패|BUILD FAILED'; then + log "⚠️ UI는 응답하나 이번 배포 로그에 실패가 있습니다" + echo "${recent}" | tail -40 + exit 1 + fi + if echo "${recent}" | grep -q '배포 완료'; then + log "✅ UI 응답 + 배포 완료 로그: ${PUBLIC_URL}" + ssh_cmd "tail -8 '${DEPLOY_LOG}'" 2>/dev/null || true + exit 0 + fi + # start-only 직후 UI 가 이전 빌드로 살아 있을 수 있음 — 신규 성공 로그를 우선 + if echo "${recent}" | grep -q '배포 시작'; then + : + else + log "✅ UI 응답: ${PUBLIC_URL}" + ssh_cmd "tail -8 '${DEPLOY_LOG}'" 2>/dev/null || true + exit 0 + fi + fi + + if echo "${recent}" | grep -q '배포 완료'; then + last_line="$(ssh_cmd "tail -1 '${DEPLOY_LOG}'" 2>/dev/null || true)" + if [[ "${last_line}" == *"배포 완료"* ]] || [[ "${last_line}" == *"락 해제"* ]]; then + # 성공 후 trap 이 락 해제를 찍을 수 있음 + if echo "${recent}" | grep -qE '\[ERROR\] 배포 실패|BUILD FAILED'; then + log "⚠️ 배포 실패 로그 감지" + echo "${recent}" | tail -40 + exit 1 + fi + log "✅ 배포 로그 확인" + ssh_cmd "tail -8 '${DEPLOY_LOG}'" 2>/dev/null || true + exit 0 + fi + fi + sleep 5 +done + +log "⚠️ 배포 대기 타임아웃 — 로그 확인: ssh ${EXDEV_SSH} tail -50 ${DEPLOY_LOG}" +new_log_tail | tail -40 +exit 1