#!/usr/bin/env bash set -euo pipefail ROOT_DIR="${ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" cd "$ROOT_DIR" export HOME="${HOME:-/root}" LOG_FILE="${TESTBOX_AUTODEPLOY_LOG_FILE:-$ROOT_DIR/logs/auto-deploy.log}" LOCK_FILE="${TESTBOX_DEPLOY_LOCK_FILE:-$ROOT_DIR/run/deploy.lock}" mkdir -p "$ROOT_DIR/logs" "$ROOT_DIR/run" env_value() { python3 "$ROOT_DIR/scripts/env-value.py" "$ROOT_DIR/.env" "$1" "${2:-}" } log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" } compose() { docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml "$@" } deployment_config_hash() { { sha256sum .env docker-compose.infra.yml docker-compose.apps.yml compose config } | sha256sum | awk '{print $1}' } ensure_config_unchanged() { local expected="$1" local phase="$2" local actual actual="$(deployment_config_hash)" if [ "$actual" != "$expected" ]; then log "deploy config changed during $phase expected=$expected actual=$actual" return 1 fi } image_ref_for_service() { case "$1" in chatapp-cron) echo "chatapp-test/chatapp-cron:testbox" ;; *) echo "chatapp-test/$1:testbox" ;; esac } service_image_id() { docker image inspect -f '{{.Id}}' "$(image_ref_for_service "$1")" } verify_running_service_image() { local service="$1" local expected_image="$2" local container_id local actual_image container_id="$(compose ps -q "$service")" if [ -z "$container_id" ]; then log "service container missing after restart service=$service" return 1 fi actual_image="$(docker inspect -f '{{.Image}}' "$container_id")" if [ "$actual_image" != "$expected_image" ]; then log "service image mismatch service=$service expected=$expected_image actual=$actual_image" return 1 fi log "service image verified service=$service image=$actual_image" } repo_changed() { local dir="$1" local url="$2" local branch="$3" local label="$4" local before="" local after="" mkdir -p "$(dirname "$dir")" if [ -e "$dir" ] && [ ! -d "$dir/.git" ]; then mv "$dir" "$dir.pre-auto-git-$(date +%Y%m%d%H%M%S)" fi if [ ! -d "$dir/.git" ]; then log "$label clone branch=$branch" git clone --branch "$branch" "$url" "$dir" git -C "$dir" rev-parse HEAD >"$ROOT_DIR/run/$label.after" return 0 fi before="$(git -C "$dir" rev-parse HEAD 2>/dev/null || true)" if ! git -C "$dir" fetch --prune origin "$branch" >>"$LOG_FILE" 2>&1; then log "$label fetch failed branch=$branch" return 2 fi git -C "$dir" checkout -B "$branch" "origin/$branch" >>"$LOG_FILE" 2>&1 git -C "$dir" reset --hard "origin/$branch" >>"$LOG_FILE" 2>&1 after="$(git -C "$dir" rev-parse HEAD)" printf '%s\n' "$before" >"$ROOT_DIR/run/$label.before" printf '%s\n' "$after" >"$ROOT_DIR/run/$label.after" if [ "$before" != "$after" ]; then log "$label changed ${before:-none} -> $after" return 0 fi return 10 } unique_words() { tr ' ' '\n' | awk 'NF && !seen[$0]++' | tr '\n' ' ' | sed 's/[[:space:]]*$//' } all_java_services() { printf '%s\n' "auth gateway external console other live wallet order" } java_module_for_service() { case "$1" in auth) echo "rc-auth" ;; gateway) echo "rc-gateway" ;; external) echo "rc-service/rc-service-external/external-start" ;; console) echo "rc-service/rc-service-console/console-start" ;; other) echo "rc-service/rc-service-other/other-start" ;; live) echo "rc-service/rc-service-live/live-start" ;; wallet) echo "rc-service/rc-service-wallet/wallet-start" ;; order) echo "rc-service/rc-service-order/order-start" ;; *) return 1 ;; esac } clean_java_service_jars() { local java_dir="$1" shift local service for service in "$@"; do case "$service" in auth) rm -f "$java_dir/rc-auth/target/rc-auth-"*.jar "$java_dir/rc-auth/target/rc-auth-"*.jar.original ;; gateway) rm -f "$java_dir/rc-gateway/target/rc-gateway-"*.jar "$java_dir/rc-gateway/target/rc-gateway-"*.jar.original ;; external) rm -f "$java_dir/rc-service/rc-service-external/external-start/target/rc-service-external-"*.jar "$java_dir/rc-service/rc-service-external/external-start/target/rc-service-external-"*.jar.original ;; console) rm -f "$java_dir/rc-service/rc-service-console/console-start/target/rc-service-console-"*.jar "$java_dir/rc-service/rc-service-console/console-start/target/rc-service-console-"*.jar.original ;; other) rm -f "$java_dir/rc-service/rc-service-other/other-start/target/rc-service-other-"*.jar "$java_dir/rc-service/rc-service-other/other-start/target/rc-service-other-"*.jar.original ;; live) rm -f "$java_dir/rc-service/rc-service-live/live-start/target/rc-service-live-"*.jar "$java_dir/rc-service/rc-service-live/live-start/target/rc-service-live-"*.jar.original ;; wallet) rm -f "$java_dir/rc-service/rc-service-wallet/wallet-start/target/rc-service-wallet-"*.jar "$java_dir/rc-service/rc-service-wallet/wallet-start/target/rc-service-wallet-"*.jar.original ;; order) rm -f "$java_dir/rc-service/rc-service-order/order-start/target/rc-service-order-"*.jar "$java_dir/rc-service/rc-service-order/order-start/target/rc-service-order-"*.jar.original ;; *) return 1 ;; esac done } service_for_java_path() { case "$1" in rc-auth/*|nacos_config/rc-auth/*) echo "auth" ;; rc-gateway/*|nacos_config/rc-gateway/*) echo "gateway" ;; rc-service/rc-service-external/*|nacos_config/rc-service-external/*) echo "external" ;; rc-service/rc-service-console/*|nacos_config/rc-service-console/*) echo "console" ;; rc-service/rc-service-other/*|nacos_config/rc-service-other/*) echo "other" ;; rc-service/rc-service-live/*|nacos_config/rc-service-live/*) echo "live" ;; rc-service/rc-service-wallet/*|nacos_config/rc-service-wallet/*) echo "wallet" ;; rc-service/rc-service-order/*|nacos_config/rc-service-order/*) echo "order" ;; nacos_config/common/*|pom.xml|*/pom.xml|rc-common*/*|rc-service/rc-inner-api/*|ci/*) all_java_services ;; sql/*) ;; *) all_java_services ;; esac } java_changed_services() { local before="$1" local after="$2" local java_dir="$3" local changed_paths local services="" local path if [ -z "$before" ]; then all_java_services return 0 fi changed_paths="$(git -C "$java_dir" diff --name-only "$before" "$after" || true)" while IFS= read -r path; do [ -n "$path" ] || continue services="$services $(service_for_java_path "$path" | tr '\n' ' ')" done <<< "$changed_paths" printf '%s\n' "$services" | unique_words } build_and_restart_java() { local java_dir="$1" shift local services=("$@") local modules="" local compose_services="" local config_hash local image_manifest local service local image_id [ "${#services[@]}" -gt 0 ] || return 0 for service in "${services[@]}"; do modules="${modules:+$modules,}$(java_module_for_service "$service")" compose_services="$compose_services $service" done config_hash="$(deployment_config_hash)" log "java build services=${services[*]} modules=$modules" cd "$java_dir" if [ -x ./ci/install-private-maven.sh ]; then ./ci/install-private-maven.sh >>"$LOG_FILE" 2>&1 fi clean_java_service_jars "$java_dir" "${services[@]}" mvn -Ptest -T 1C -pl "$modules" -am -Dmaven.test.skip=true package >>"$LOG_FILE" 2>&1 cd "$ROOT_DIR" python3 scripts/import-nacos-configs.py >>"$LOG_FILE" 2>&1 TESTBOX_APPLY_JAVA_SQL=1 scripts/apply-mysql-baseline.sh >>"$LOG_FILE" 2>&1 compose build "${services[@]}" >>"$LOG_FILE" 2>&1 ensure_config_unchanged "$config_hash" "java image build" image_manifest="$ROOT_DIR/run/deploy-images-java-$$.txt" : >"$image_manifest" for service in "${services[@]}"; do image_id="$(service_image_id "$service")" printf '%s %s\n' "$service" "$image_id" >>"$image_manifest" log "java image built service=$service image=$image_id" done for service in "${services[@]}"; do log "java restart service=$service" compose up -d --no-deps --force-recreate "$service" >>"$LOG_FILE" 2>&1 done ensure_config_unchanged "$config_hash" "java service restart" while read -r service image_id; do verify_running_service_image "$service" "$image_id" done <"$image_manifest" rm -f "$image_manifest" compose ps "${services[@]}" | tee -a "$LOG_FILE" } build_and_restart_golang() { local config_hash local image_id config_hash="$(deployment_config_hash)" log "golang build/restart" scripts/apply-mysql-baseline.sh >>"$LOG_FILE" 2>&1 compose build golang >>"$LOG_FILE" 2>&1 ensure_config_unchanged "$config_hash" "golang image build" image_id="$(service_image_id golang)" log "golang image built image=$image_id" compose up -d --no-deps --force-recreate golang >>"$LOG_FILE" 2>&1 ensure_config_unchanged "$config_hash" "golang service restart" verify_running_service_image golang "$image_id" compose ps golang | tee -a "$LOG_FILE" } build_and_restart_cron() { local config_hash local image_id config_hash="$(deployment_config_hash)" log "cron build/restart" compose build chatapp-cron >>"$LOG_FILE" 2>&1 ensure_config_unchanged "$config_hash" "cron image build" image_id="$(service_image_id chatapp-cron)" log "cron image built image=$image_id" compose up -d --no-deps --force-recreate chatapp-cron >>"$LOG_FILE" 2>&1 ensure_config_unchanged "$config_hash" "cron service restart" verify_running_service_image chatapp-cron "$image_id" compose ps chatapp-cron | tee -a "$LOG_FILE" } build_and_restart_admin() { local admin_dir="$1" local admin_api_base_url local admin_oss_url local config_hash local image_id config_hash="$(deployment_config_hash)" admin_api_base_url="$(env_value TESTBOX_ADMIN_API_BASE_URL "https://yumi-test-admin.haiyihy.com/console")" admin_oss_url="$(env_value TESTBOX_ADMIN_OSS_URL "")" log "admin build/restart" docker build \ -f "$ROOT_DIR/docker/chatapp3-admin.Dockerfile" \ --build-arg "ADMIN_API_BASE_URL=$admin_api_base_url" \ --build-arg "ADMIN_OSS_URL=$admin_oss_url" \ -t chatapp-test/admin:testbox \ "$admin_dir" >>"$LOG_FILE" 2>&1 ensure_config_unchanged "$config_hash" "admin image build" image_id="$(service_image_id admin)" log "admin image built image=$image_id" compose up -d --no-deps --force-recreate admin >>"$LOG_FILE" 2>&1 ensure_config_unchanged "$config_hash" "admin service restart" verify_running_service_image admin "$image_id" compose ps admin | tee -a "$LOG_FILE" } run_once() { local java_repo_url golang_repo_url cron_repo_url admin_repo_url local java_branch golang_branch cron_branch admin_branch local java_dir golang_dir cron_dir admin_dir local rc local java_before java_after local services_line exec 9>&- 2>/dev/null || true exec 9>"$LOCK_FILE" if ! flock -n 9; then log "deploy lock busy; skip this tick" exec 9>&- return 0 fi java_repo_url="$(env_value JAVA_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-java.git)" golang_repo_url="$(env_value GOLANG_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-golang.git)" cron_repo_url="$(env_value CRON_REPO_URL git@gitea.haiyihy.com:hy/chatapp-cron.git)" admin_repo_url="$(env_value ADMIN_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-admin.git)" java_branch="$(env_value TESTBOX_AUTODEPLOY_JAVA_BRANCH test)" golang_branch="$(env_value TESTBOX_AUTODEPLOY_GOLANG_BRANCH test)" cron_branch="$(env_value TESTBOX_AUTODEPLOY_CRON_BRANCH test)" admin_branch="$(env_value TESTBOX_AUTODEPLOY_ADMIN_BRANCH test)" java_dir="$(env_value JAVA_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-java")" golang_dir="$(env_value GOLANG_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-golang")" cron_dir="$(env_value CRON_SOURCE_DIR "$ROOT_DIR/sources/chatapp-cron")" admin_dir="$(env_value ADMIN_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-admin")" rc=0 if repo_changed "$java_dir" "$java_repo_url" "$java_branch" java; then java_before="$(cat "$ROOT_DIR/run/java.before" 2>/dev/null || true)" java_after="$(cat "$ROOT_DIR/run/java.after" 2>/dev/null || true)" services_line="$(java_changed_services "$java_before" "$java_after" "$java_dir")" if [ -n "$services_line" ]; then # shellcheck disable=SC2206 local java_services=($services_line) build_and_restart_java "$java_dir" "${java_services[@]}" else log "java changed without service rebuild target; applying SQL/Nacos only" python3 scripts/import-nacos-configs.py >>"$LOG_FILE" 2>&1 TESTBOX_APPLY_JAVA_SQL=1 scripts/apply-mysql-baseline.sh >>"$LOG_FILE" 2>&1 fi else rc=$? [ "$rc" -eq 10 ] || log "java update skipped due to error rc=$rc" fi rc=0 if repo_changed "$golang_dir" "$golang_repo_url" "$golang_branch" golang; then build_and_restart_golang else rc=$? [ "$rc" -eq 10 ] || log "golang update skipped due to error rc=$rc" fi rc=0 if repo_changed "$cron_dir" "$cron_repo_url" "$cron_branch" cron; then build_and_restart_cron else rc=$? [ "$rc" -eq 10 ] || log "cron update skipped due to error rc=$rc" fi rc=0 if repo_changed "$admin_dir" "$admin_repo_url" "$admin_branch" admin; then build_and_restart_admin "$admin_dir" else rc=$? [ "$rc" -eq 10 ] || log "admin update skipped due to error rc=$rc" fi flock -u 9 exec 9>&- } usage() { cat <<'EOF' Usage: auto-pull-restart.sh # loop forever auto-pull-restart.sh --once # run one poll cycle Reads TESTBOX_AUTODEPLOY_* from /opt/chatapp-test/.env. EOF } case "${1:-}" in --once) run_once ;; -h|--help) usage ;; "") interval="$(env_value TESTBOX_AUTODEPLOY_INTERVAL_SECONDS 30)" log "auto deploy loop started interval=${interval}s" while true; do if ! run_once; then log "auto deploy tick failed" fi sleep "$interval" done ;; *) usage >&2 exit 2 ;; esac