#!/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 생략) # # commit 시 운영 WAS/RAG 기본 주소(172.16.180.130)를 복원합니다. # exdev 런타임 주소(127.0.0.1 / host.docker.internal)는 .env·start 스크립트·EXDEV_LOCAL 로만 사용하며 # 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) WAS 주소를 커밋하지 않고 운영 기본값(172.16.180.130) 유지 # - 런타임(exdev): start-all-server.sh / .env / EXDEV_LOCAL=true → 127.0.0.1·host.docker.internal # - 커밋/push: 아래 복원으로 application.yml·env.template·chatbot_tool_client.py 기본값을 운영 IP로 고정 PROD_WAS_API="http://172.16.180.130:8086/api" PROD_RAG_CHAT="http://172.16.180.130:28012/agent/chat" restore_production_endpoints_before_commit() { local path="$1" local dir="${ROOT_DIR}/${path}" case "${path}" in kakaoChatbotSkill-chatbot2.0-agent) local yml="${dir}/src/main/resources/application.yml" if [[ -f "${yml}" ]]; then # ${ENV:default} 형태의 default 만 운영 IP로 고정 (실행 시 ENV 로컬 오버라이드는 유지) perl -i -pe "s|CHATBOT_API_BASE_URL:[^}]+}|CHATBOT_API_BASE_URL:${PROD_WAS_API}/}|g" "${yml}" perl -i -pe "s|RAG_AGENT_CHAT_URL:[^}]+}|RAG_AGENT_CHAT_URL:${PROD_RAG_CHAT}}|g" "${yml}" # trailing slash normalize for api default perl -i -pe "s|CHATBOT_API_BASE_URL:http://172\.16\.180\.130:8086/api/\}|CHATBOT_API_BASE_URL:http://172.16.180.130:8086/api/}|g" "${yml}" if grep -qE '127\.0\.0\.1:8086|host\.docker\.internal:8086|127\.0\.0\.1:28012' "${yml}"; then warn "${path}: application.yml 에 로컬 주소가 남아 있을 수 있음 — 확인 필요" fi fi ;; exAiChatBot-chatbot2.0-agent) if [[ -f "${dir}/env.template" ]]; then perl -i -pe "s|^CHATBOT_API_BASE_URL=.*|CHATBOT_API_BASE_URL=${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 perl -i -pe "s|^CHATBOT_API_BASE_URL=.*|CHATBOT_API_BASE_URL=${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)만 복원. # _EXDEV_WAS_API(host.docker.internal) 는 exdev 차단용이라 건드리지 않는다. perl -i -pe 's|_PROD_WAS_API = "http://[^"]+"|_PROD_WAS_API = "http://172.16.180.130:8086/api"|g' \ "${dir}/scripts/agent/chatbot_tool_client.py" perl -i -pe 's|os\.getenv\("TOOL_API_BASE_URL", "http://127\.0\.0\.1:8086/api"\)|os.getenv("TOOL_API_BASE_URL", "http://172.16.180.130:8086/api")|g' \ "${dir}/scripts/agent/chatbot_tool_client.py" perl -i -pe 's|os\.getenv\("TOOL_API_BASE_URL", "http://host\.docker\.internal:8086/api"\)|os.getenv("TOOL_API_BASE_URL", "http://172.16.180.130:8086/api")|g' \ "${dir}/scripts/agent/chatbot_tool_client.py" # 구형 한 줄 기본값 형태도 운영 IP로 고정 perl -i -pe 's|os\.getenv\("TOOL_API_BASE_URL", "http://[^"]+"\)|os.getenv("TOOL_API_BASE_URL", "http://172.16.180.130:8086/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 ;; esac } 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 if $DO_COMMIT && repo_is_dirty; then log " 로컬 변경 commit..." # push 전에 운영 WAS/RAG 기본 주소 복원 (exdev 로컬 주소는 .env / start 스크립트로만 사용) restore_production_endpoints_before_commit "${path}" log " 운영 엔드포인트 기본값 유지: ${PROD_WAS_API}" git add -A if git diff --cached --quiet; then log " staged 변경 없음 (ignore 된 파일만 있을 수 있음)" else 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"