#!/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 / host.docker.internal)를 적용한다. # 이 스크립트는 commit 직전에 운영 기본값(172.16.180.130)을 파일에 복원한 뒤 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" 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}" 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' \ "${dir}/src/main/resources/application.yml" 2>/dev/null || true)" ;; 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"