#!/usr/bin/env bash if [ -z "${BASH_VERSION:-}" ]; then exec bash "$0" "$@" fi set -Eeuo pipefail APP_NAME="${APP_NAME:-pc_nuxt}" BRANCH="${BRANCH:-}" NUXT_PUBLIC_API_BASE="${NUXT_PUBLIC_API_BASE:-https://api.aionline.cc}" NUXT_API_SERVER_BASE="${NUXT_API_SERVER_BASE:-http://127.0.0.1:16888}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export NUXT_PUBLIC_API_BASE export NUXT_API_SERVER_BASE cd "$SCRIPT_DIR" log() { printf '\n[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" } require_command() { if ! command -v "$1" >/dev/null 2>&1; then log "Missing command: $1" exit 1 fi } validate_api_base() { node - "$NUXT_PUBLIC_API_BASE" <<'NODE' const value = process.argv[2] let url try { url = new URL(value) } catch { console.error(`[pc_nuxt] Invalid NUXT_PUBLIC_API_BASE: ${value || '(empty)'}`) process.exit(1) } const hostname = url.hostname.toLowerCase() const isPrivate = hostname === 'localhost' || hostname === '0.0.0.0' || /^127\./.test(hostname) || hostname === '[::1]' || hostname.endsWith('.local') || /^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^169\.254\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) if (!['http:', 'https:'].includes(url.protocol) || isPrivate) { console.error(`[pc_nuxt] Production API must be a public http(s) URL: ${value}`) process.exit(1) } NODE } validate_server_api_base() { node - "$NUXT_API_SERVER_BASE" <<'NODE' const value = process.argv[2] let url try { url = new URL(value) } catch { console.error(`[pc_nuxt] Invalid NUXT_API_SERVER_BASE: ${value || '(empty)'}`) process.exit(1) } if (!['http:', 'https:'].includes(url.protocol)) { console.error(`[pc_nuxt] NUXT_API_SERVER_BASE must be http(s): ${value}`) process.exit(1) } NODE } verify_server_api() { log "Verify SSR API connectivity" node - "$NUXT_API_SERVER_BASE" <<'NODE' const http = require('node:http') const https = require('node:https') const base = process.argv[2] const target = new URL('/article/getlist', `${base.replace(/\/+$/, '')}/`) const body = new URLSearchParams({ mac: 'pc-nuxt-deploy-check', base_timestamp: String(Math.floor(Date.now() / 1000)), client: '1', client_ios: '0', version: '1.0.0', version_code: '1', page_no: '1', page_size: '1' }).toString() const transport = target.protocol === 'https:' ? https : http const request = transport.request(target, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Content-Length': Buffer.byteLength(body) }, timeout: 10000 }, (response) => { const chunks = [] response.on('data', chunk => chunks.push(chunk)) response.on('end', () => { const text = Buffer.concat(chunks).toString('utf8') let result try { result = JSON.parse(text) } catch { console.error(`[pc_nuxt] SSR API returned invalid JSON (${response.statusCode}): ${text.slice(0, 200)}`) process.exit(1) } if (response.statusCode < 200 || response.statusCode >= 300 || result.code !== 0) { console.error(`[pc_nuxt] SSR API check failed (${response.statusCode}): ${result.msg || text.slice(0, 200)}`) process.exit(1) } console.log(`[pc_nuxt] SSR API OK: ${base}`) }) }) request.on('timeout', () => request.destroy(new Error('timeout'))) request.on('error', (error) => { console.error(`[pc_nuxt] SSR API unavailable: ${base} (${error.message})`) process.exit(1) }) request.write(body) request.end() NODE } validate_tls_security() { if [ "${NODE_TLS_REJECT_UNAUTHORIZED:-}" = "0" ]; then log "Refusing deployment: NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification." exit 1 fi } resolve_branch() { if [ -z "$BRANCH" ]; then BRANCH="$(git rev-parse --abbrev-ref HEAD)" fi if [ "$BRANCH" = "HEAD" ]; then log "Cannot detect git branch. Please run with BRANCH=main ./restart.sh" exit 1 fi } sync_latest_code() { log "Force sync code from origin/$BRANCH" git fetch origin "$BRANCH" if [ -n "$(git status --porcelain)" ]; then log "Discard local working tree changes before deploy" git status --short fi git reset --hard "origin/$BRANCH" git clean -fd } install_dependencies() { log "Install dependencies" pnpm install --frozen-lockfile } build_app() { log "Build Nuxt app" pnpm run build if [ ! -f ".output/server/index.mjs" ]; then log "Build output missing: .output/server/index.mjs" exit 1 fi } restart_app() { log "Reload app with pm2 ecosystem config" mkdir -p logs APP_NAME="$APP_NAME" pm2 startOrReload ecosystem.config.cjs --env production --update-env pm2 save >/dev/null 2>&1 || true } main() { require_command git require_command node require_command pnpm require_command pm2 validate_tls_security validate_api_base validate_server_api_base resolve_branch log "Deploy start: $APP_NAME" log "Node: $(node -v 2>/dev/null || echo 'unknown')" log "pnpm: $(pnpm -v)" log "pm2: $(pm2 -v)" log "API: $NUXT_PUBLIC_API_BASE" log "Server API: $NUXT_API_SERVER_BASE" verify_server_api sync_latest_code install_dependencies build_app restart_app log "Deploy done: $APP_NAME" } main "$@"