#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REMOTE_HOST="${REMOTE_HOST:-ubt}" DEPLOY_DIR="${DEPLOY_DIR:-/root/docker/newapi}" APP_CONTAINER="${APP_CONTAINER:-new-api}" APP_PORT="${APP_PORT:-3000}" PUBLIC_URL="${PUBLIC_URL:-https://leexx.site}" NODE_IMAGE="${NODE_IMAGE:-node:22-bullseye}" GO_IMAGE="${GO_IMAGE:-golang:1.26.1-alpine}" GOPROXY_VALUE="${GOPROXY_VALUE:-https://goproxy.cn,direct}" FRONT_BUILD_HEAP_MB="${FRONT_BUILD_HEAP_MB:-8192}" SYNC_EXCLUDES=( ".git" ".env" "docker-compose.yml" "data" "logs" "redis" "backups" ".update_tmp" "web/node_modules" ) usage() { cat <<'EOF' Usage: scripts/deploy-ubt.sh Optional environment variables: REMOTE_HOST SSH host alias, default: ubt DEPLOY_DIR Remote deploy dir, default: /root/docker/newapi APP_CONTAINER Container name, default: new-api APP_PORT Local bind port on remote host, default: 3000 PUBLIC_URL Public URL for final check, default: https://leexx.site NODE_IMAGE Node image for frontend build, default: node:22-bullseye GO_IMAGE Go image for backend build, default: golang:1.26.1-alpine GOPROXY_VALUE GOPROXY for remote build, default: https://goproxy.cn,direct FRONT_BUILD_HEAP_MB Node heap limit in MB, default: 8192 EOF } log() { printf '[deploy-ubt] %s\n' "$*" } require_cmd() { command -v "$1" >/dev/null 2>&1 || { printf 'Missing required command: %s\n' "$1" >&2 exit 1 } } remote_bash() { local script="$1" shift ssh "$REMOTE_HOST" 'bash -s' -- "$@" <<<"$script" } sync_source() { log "Syncing source to ${REMOTE_HOST}:${DEPLOY_DIR}" local rsync_args=( -az --delete ) local exclude for exclude in "${SYNC_EXCLUDES[@]}"; do rsync_args+=(--exclude "$exclude") done rsync "${rsync_args[@]}" "${ROOT_DIR}/" "${REMOTE_HOST}:${DEPLOY_DIR}/" } verify_remote_layout() { log "Checking remote layout" remote_bash ' set -euo pipefail deploy_dir="$1" test -d "$deploy_dir" test -f "$deploy_dir/.env" test -f "$deploy_dir/docker-compose.yml" test -f "$deploy_dir/web/package.json" test -f "$deploy_dir/web/package-lock.json" test -f "$deploy_dir/web/src/helpers/render.jsx" test -d "$deploy_dir/data" test -d "$deploy_dir/logs" ' "$DEPLOY_DIR" } build_frontend() { log "Building frontend dist on ${REMOTE_HOST}" remote_bash ' set -euo pipefail deploy_dir="$1" node_image="$2" heap_mb="$3" docker run --rm --network host \ -v "${deploy_dir}:/src" \ "${node_image}" \ sh -lc " set -e rm -rf /tmp/web cp -a /src/web /tmp/web cd /tmp/web npm ci --legacy-peer-deps NODE_OPTIONS=--max-old-space-size=${heap_mb} npm run build rm -rf /src/web/dist cp -a dist /src/web/dist " ' "$DEPLOY_DIR" "$NODE_IMAGE" "$FRONT_BUILD_HEAP_MB" } build_backend() { log "Building backend binary on ${REMOTE_HOST}" remote_bash ' set -euo pipefail deploy_dir="$1" go_image="$2" goproxy_value="$3" docker run --rm --network host \ -e "GOPROXY=${goproxy_value}" \ -v "${deploy_dir}:/src" \ -w /src \ "${go_image}" \ sh -lc " set -e export PATH=/usr/local/go/bin:\$PATH VERSION_VALUE=\$(cat VERSION 2>/dev/null || true) go mod download go build -ldflags \"-s -w -X github.com/QuantumNous/new-api/common.Version=\${VERSION_VALUE}\" -o new-api " ' "$DEPLOY_DIR" "$GO_IMAGE" "$GOPROXY_VALUE" } swap_binary() { log "Replacing binary in ${APP_CONTAINER}" remote_bash ' set -euo pipefail deploy_dir="$1" app_container="$2" backup_dir="${deploy_dir}/backups" mkdir -p "$backup_dir" ts="$(date +%Y%m%d%H%M%S)" docker inspect "$app_container" >/dev/null 2>&1 docker cp "${app_container}:/new-api" "${backup_dir}/new-api.${ts}.bak" || true docker cp "${deploy_dir}/new-api" "${app_container}:/new-api" docker restart "$app_container" >/dev/null ' "$DEPLOY_DIR" "$APP_CONTAINER" } verify_deploy() { log "Checking local health endpoint" remote_bash ' set -euo pipefail app_container="$1" app_port="$2" public_url="$3" local_url="http://127.0.0.1:${app_port}/api/status" ok=0 for _ in $(seq 1 45); do if curl -fsS "$local_url" >/dev/null 2>&1; then ok=1 break fi sleep 2 done if [[ "$ok" != "1" ]]; then echo "Health check failed: ${local_url}" >&2 docker logs --tail 120 "$app_container" || true exit 1 fi docker ps --filter "name=${app_container}" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" curl -fsS "$local_url" >/dev/null curl -I -m 20 "$public_url" | sed -n "1,12p" ' "$APP_CONTAINER" "$APP_PORT" "$PUBLIC_URL" } main() { if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage exit 0 fi require_cmd ssh require_cmd rsync log "Starting deploy to ${REMOTE_HOST}" sync_source verify_remote_layout build_frontend build_backend swap_binary verify_deploy log "Deploy completed successfully" } main "$@"