#!/usr/bin/env bash set -Eeuo pipefail # Existing data-disk installations must fail before any initialization or release writes. if [[ -e /etc/cmpp-platform/data-disk.uuid || -e /etc/systemd/system/postgresql.service.d/50-cmpp-data-disk.conf ]]; then if [[ ! -x /usr/local/sbin/cmpp-data-storage-check ]]; then echo "Missing CMPP data-disk guard; refusing to continue." >&2 exit 1 fi /usr/local/sbin/cmpp-data-storage-check all fi APP_DIR="${APP_DIR:-/opt/cmpp-platform}" ENV_FILE="${ENV_FILE:-/etc/cmpp-platform/cmpp-platform.env}" ADMIN_CREDENTIAL_FILE="${ADMIN_CREDENTIAL_FILE:-/root/cmpp-platform-admin.txt}" if [[ "$(id -u)" -ne 0 ]]; then echo "Run as root." >&2 exit 1 fi if [[ ! -f "$ENV_FILE" ]]; then echo "Missing environment file: $ENV_FILE" >&2 exit 1 fi set -a source "$ENV_FILE" set +a if [[ ! "${API_HTTP_KEEP_ALIVE_TIMEOUT_MS:-}" =~ ^[1-9][0-9]*$ || ! "${API_HTTP_HEADERS_TIMEOUT_MS:-}" =~ ^[1-9][0-9]*$ || "${API_HTTP_HEADERS_TIMEOUT_MS}" -le "${API_HTTP_KEEP_ALIVE_TIMEOUT_MS}" ]]; then echo "API_HTTP_KEEP_ALIVE_TIMEOUT_MS must be positive and API_HTTP_HEADERS_TIMEOUT_MS must be greater in $ENV_FILE." >&2 exit 1 fi if [[ "${API_ENABLE_SEND_WORKER:-}" != "true" ]]; then echo "API_ENABLE_SEND_WORKER=true is required in $ENV_FILE; refusing to deploy with SMS sending disabled." >&2 exit 1 fi if [[ ! "${API_SEND_WORKER_CONCURRENCY:-}" =~ ^[1-9][0-9]*$ ]]; then echo "API_SEND_WORKER_CONCURRENCY must be a positive integer in $ENV_FILE." >&2 exit 1 fi if [[ "${CMPP_INBOUND_FAST_PATH_ENABLED:-}" != "true" || "${CMPP_INBOUND_WORKFLOW_WORKER_ENABLED:-}" != "true" ]]; then echo "CMPP_INBOUND_FAST_PATH_ENABLED=true and CMPP_INBOUND_WORKFLOW_WORKER_ENABLED=true are required in $ENV_FILE." >&2 exit 1 fi if [[ ! "${API_INBOUND_WORKFLOW_CONCURRENCY:-}" =~ ^[1-9][0-9]*$ ]]; then echo "API_INBOUND_WORKFLOW_CONCURRENCY must be a positive integer in $ENV_FILE." >&2 exit 1 fi if [[ "${API_INBOUND_WORKFLOW_BATCH_ENABLED:-}" != "true" || ! "${API_INBOUND_WORKFLOW_BATCH_SIZE:-}" =~ ^[1-9][0-9]*$ ]]; then echo "API_INBOUND_WORKFLOW_BATCH_ENABLED=true and a positive API_INBOUND_WORKFLOW_BATCH_SIZE are required in $ENV_FILE." >&2 exit 1 fi if [[ ! "${API_INBOUND_WORKFLOW_BATCH_WAIT_MS:-}" =~ ^[0-9]+$ || "${API_INBOUND_WORKFLOW_BATCH_WAIT_MS}" -gt 250 ]]; then echo "API_INBOUND_WORKFLOW_BATCH_WAIT_MS must be between 0 and 250 in $ENV_FILE." >&2 exit 1 fi if [[ ! "${API_INBOUND_WORKFLOW_TARGET_BATCH_SIZE:-}" =~ ^[1-9][0-9]*$ || "${API_INBOUND_WORKFLOW_TARGET_BATCH_SIZE}" -gt "${API_INBOUND_WORKFLOW_BATCH_SIZE}" ]]; then echo "API_INBOUND_WORKFLOW_TARGET_BATCH_SIZE must be positive and no greater than API_INBOUND_WORKFLOW_BATCH_SIZE in $ENV_FILE." >&2 exit 1 fi if [[ ! "${API_DB_POOL_MAX:-}" =~ ^[1-9][0-9]*$ || ! "${API_WORKER_DB_POOL_MAX:-}" =~ ^[1-9][0-9]*$ ]]; then echo "API_DB_POOL_MAX and API_WORKER_DB_POOL_MAX must be positive integers in $ENV_FILE." >&2 exit 1 fi if [[ "${SEND_SUBMIT_OUTBOX_SEPARATE_PROCESS_ENABLED:-false}" == "true" && ! "${API_OUTBOX_DB_POOL_MAX:-}" =~ ^[1-9][0-9]*$ ]]; then echo "API_OUTBOX_DB_POOL_MAX must be a positive integer when the separate Submit Outbox publisher is enabled." >&2 exit 1 fi if [[ "${GATEWAY_CALLBACK_SEPARATE_PROCESS_ENABLED:-false}" == "true" ]]; then if [[ ! "${API_CALLBACK_DB_POOL_MAX:-}" =~ ^[1-9][0-9]*$ || ! "${API_CALLBACK_PORT:-}" =~ ^[1-9][0-9]*$ ]]; then echo "API_CALLBACK_DB_POOL_MAX and API_CALLBACK_PORT must be positive integers when the separate Gateway callback is enabled." >&2 exit 1 fi expected_callback_url="http://127.0.0.1:${API_CALLBACK_PORT}/api" if [[ "${GATEWAY_CALLBACK_API_BASE_URL:-}" != "$expected_callback_url" ]]; then echo "GATEWAY_CALLBACK_API_BASE_URL must equal $expected_callback_url for the loopback callback process." >&2 exit 1 fi fi if [[ "${GATEWAY_PROTOCOL_LOG_STREAM_ENABLED:-false}" == "true" && ! "${API_PROTOCOL_LOG_DB_POOL_MAX:-}" =~ ^[1-9][0-9]*$ ]]; then echo "API_PROTOCOL_LOG_DB_POOL_MAX must be a positive integer when protocol log Stream writing is enabled." >&2 exit 1 fi if [[ "${GATEWAY_CALLBACK_BATCH_ENABLED:-false}" == "true" ]]; then if [[ ! "${GATEWAY_CALLBACK_BATCH_SIZE:-}" =~ ^[1-9][0-9]*$ || "${GATEWAY_CALLBACK_BATCH_SIZE}" -gt 100 ]]; then echo "GATEWAY_CALLBACK_BATCH_SIZE must be between 1 and 100." >&2 exit 1 fi fi if [[ -z "${CMPP_PUBLIC_HOST:-}" || ! "${CMPP_PUBLIC_PORT:-}" =~ ^[1-9][0-9]*$ ]]; then echo "CMPP_PUBLIC_HOST and a positive CMPP_PUBLIC_PORT are required in $ENV_FILE; these are the customer-facing CMPP endpoint." >&2 exit 1 fi cd "$APP_DIR" echo "[deploy] Installing dependencies" npm ci --include=dev npm --prefix api ci --include=dev echo "[deploy] Verifying dependency security mitigations" npm run security:verify npm run deploy:verify echo "[deploy] Generating Prisma client and applying migrations" npm --prefix api run prisma:generate npm --prefix api run prisma:migrate:deploy echo "[deploy] Building frontend, API and gateway" rm -rf api/dist api/tsconfig.build.tsbuildinfo "$APP_DIR/dist/cmpp-gateway" npm run build npm --prefix api run build (cd gateway && GOPROXY="${GOPROXY:-https://goproxy.cn,direct}" /usr/local/bin/go build -o "$APP_DIR/dist/cmpp-gateway" ./cmd/gateway) (cd gateway && GOPROXY="${GOPROXY:-https://goproxy.cn,direct}" /usr/local/bin/go build -o "$APP_DIR/dist/cmpp-security-agent" ./cmd/security-agent) chmod 755 "$APP_DIR/dist" "$APP_DIR/dist/assets" find "$APP_DIR/dist/assets" -type d -exec chmod 755 {} + find "$APP_DIR/dist/assets" -type f -exec chmod 644 {} + chmod 644 "$APP_DIR/dist/index.html" echo "[deploy] Ensuring production admin" PROD_ADMIN_CREDENTIAL_FILE="$ADMIN_CREDENTIAL_FILE" node tools/deploy/ensure-production-admin.mjs chmod 600 "$ADMIN_CREDENTIAL_FILE" || true echo "[deploy] Ensuring runtime log directories" install -d -m 0755 "$APP_DIR/logs/api" "$APP_DIR/logs/send-worker" "$APP_DIR/logs/report-material-worker" "$APP_DIR/logs/submit-outbox" "$APP_DIR/logs/gateway-callback" "$APP_DIR/logs/protocol-log-worker" "$APP_DIR/logs/gateway" echo "[deploy] Installing split API and send-worker services" node_bin="$(command -v node)" install -d -m 0755 /etc/systemd/system/cmpp-api.service.d cat >/etc/systemd/system/cmpp-api.service.d/process-role.conf <<'EOF' [Service] Environment=CMPP_PROCESS_ROLE=api EOF cat >/etc/systemd/system/cmpp-send-worker.service </etc/systemd/system/cmpp-report-material-worker.service </etc/systemd/system/cmpp-submit-outbox.service </etc/systemd/system/cmpp-gateway-callback.service </etc/systemd/system/cmpp-protocol-log-worker.service <"$compression_config" if grep -RqsE --exclude='cmpp-compression.conf' '^[[:space:]]*gzip[[:space:]]+on;' \ /etc/nginx/nginx.conf /etc/nginx/conf.d /etc/nginx/sites-enabled 2>/dev/null; then echo "[deploy] Reusing existing Nginx gzip configuration" else cat >"$compression_config" <<'EOF' gzip on; gzip_vary on; gzip_min_length 1024; gzip_comp_level 5; gzip_types application/json application/javascript text/javascript text/css text/plain text/csv image/svg+xml; EOF fi nginx -t echo "[deploy] Restarting services" systemctl daemon-reload if [[ "${OBJECT_STORAGE_DRIVER:-minio}" == "local" ]]; then systemctl disable --now cmpp-minio 2>/dev/null || true systemctl enable --now cmpp-api cmpp-send-worker cmpp-report-material-worker cmpp-gateway nginx else systemctl enable --now cmpp-minio systemctl restart cmpp-minio systemctl enable --now cmpp-api cmpp-send-worker cmpp-report-material-worker cmpp-gateway nginx fi systemctl restart cmpp-security-agent if [[ "${SEND_SUBMIT_OUTBOX_SEPARATE_PROCESS_ENABLED:-false}" == "true" ]]; then systemctl enable --now cmpp-submit-outbox systemctl restart cmpp-submit-outbox else systemctl disable --now cmpp-submit-outbox 2>/dev/null || true fi if [[ "${GATEWAY_CALLBACK_SEPARATE_PROCESS_ENABLED:-false}" == "true" ]]; then systemctl enable --now cmpp-gateway-callback systemctl restart cmpp-gateway-callback else systemctl disable --now cmpp-gateway-callback 2>/dev/null || true fi if [[ "${GATEWAY_PROTOCOL_LOG_STREAM_ENABLED:-false}" == "true" ]]; then systemctl enable --now cmpp-protocol-log-worker systemctl restart cmpp-protocol-log-worker else systemctl disable --now cmpp-protocol-log-worker 2>/dev/null || true fi # Gateway is restarted after the callback listener, then API is restarted after # Gateway so API startup reconciliation always restores the active channel set. systemctl restart cmpp-gateway systemctl restart cmpp-api systemctl restart cmpp-send-worker systemctl restart cmpp-report-material-worker systemctl restart nginx echo "[deploy] Health checks" wait_for_http() { local name="$1" local url="$2" local attempt # Nest初始化和活动通道恢复会随生产数据量波动;固定等待会把正常的慢启动误判为发布失败。 for attempt in $(seq 1 60); do if curl -fsS "$url" >/dev/null 2>&1; then return 0 fi sleep 1 done echo "$name did not become healthy within 60 seconds: $url" >&2 return 1 } wait_for_http "API" "http://127.0.0.1:${API_PORT:-3000}/api/health" wait_for_http "Send worker metrics" "http://127.0.0.1:${API_WORKER_METRICS_PORT:-9465}/metrics" systemctl is-active --quiet cmpp-report-material-worker if [[ "${SEND_SUBMIT_OUTBOX_SEPARATE_PROCESS_ENABLED:-false}" == "true" ]]; then wait_for_http "Submit Outbox metrics" "http://127.0.0.1:${API_OUTBOX_METRICS_PORT:-9467}/metrics" fi if [[ "${GATEWAY_CALLBACK_SEPARATE_PROCESS_ENABLED:-false}" == "true" ]]; then wait_for_http "Gateway callback" "http://127.0.0.1:${API_CALLBACK_PORT}/api/health" wait_for_http "Gateway callback metrics" "http://127.0.0.1:${API_CALLBACK_METRICS_PORT:-9468}/metrics" fi wait_for_http "Gateway" "http://127.0.0.1:8090/health" redis-cli -h "${REDIS_HOST:-127.0.0.1}" -p "${REDIS_PORT:-6379}" ping >/dev/null pg_isready -d "${DATABASE_URL%%\?*}" >/dev/null echo "[deploy] Done"