#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true PROXMOX_HOST="${PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}" PROXMOX_USER="${PROXMOX_USER:-root}" VMID="${VMID:-8604}" CT_HOSTNAME="${CT_HOSTNAME:-currencicombo-phoenix-1}" CT_IP="${CT_IP:-10.160.0.14}" CT_PREFIX="${CT_PREFIX:-22}" CT_GW="${CT_GW:-10.160.0.1}" CT_VLAN_TAG="${CT_VLAN_TAG:-160}" CT_STORAGE="${CT_STORAGE:-thin1}" CT_TEMPLATE="${CT_TEMPLATE:-ubuntu-22.04-standard_22.04-1_amd64.tar.zst}" CT_MEMORY_MB="${CT_MEMORY_MB:-6144}" CT_CORES="${CT_CORES:-4}" CT_ROOTFS_GB="${CT_ROOTFS_GB:-40}" CT_SWAP_MB="${CT_SWAP_MB:-1024}" CT_TIMEZONE="${CT_TIMEZONE:-America/Los_Angeles}" CT_NODE_ENV="${CT_NODE_ENV:-production}" DEPLOY_ROOT="${DEPLOY_ROOT:-/opt/currencicombo}" REPO_URL="${REPO_URL:-https://gitea.d-bis.org/d-bis/CurrenciCombo.git}" REPO_BRANCH="${REPO_BRANCH:-main}" REPO_REF="${REPO_REF:-}" LOCAL_SRC="${CURRENCICOMBO_SRC:-}" POSTGRES_DB="${POSTGRES_DB:-comboflow}" POSTGRES_USER="${POSTGRES_USER:-comboflow}" POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-comboflow-prod-please-rotate}" REDIS_URL="${REDIS_URL:-redis://127.0.0.1:6379}" ORCH_PORT="${ORCH_PORT:-8080}" WEB_PORT="${WEB_PORT:-3000}" NEXTAUTH_URL="${NEXTAUTH_URL:-http://${CT_IP}:${WEB_PORT}}" NEXT_PUBLIC_ORCH_URL="${NEXT_PUBLIC_ORCH_URL:-http://${CT_IP}:${ORCH_PORT}}" SESSION_SECRET="${SESSION_SECRET:-currencicombo-session-secret-change-me-32chars}" JWT_SECRET="${JWT_SECRET:-currencicombo-jwt-secret-change-me-32chars}" API_KEYS="${API_KEYS:-currencicombo-phoenix-dev-key}" SSH_OPTS=(-o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new) DRY_RUN=0 APPLY=0 SKIP_CREATE=0 SKIP_SYSTEM_PACKAGES=0 TMP_ARCHIVE="$(mktemp /tmp/currencicombo-8604-XXXXXX.tgz)" REMOTE_ARCHIVE="/tmp/$(basename "$TMP_ARCHIVE")" PUSH_ARCHIVE="/root/$(basename "$TMP_ARCHIVE")" usage() { cat <&2; exit 1; } run_local() { if [[ "$DRY_RUN" == "1" ]]; then printf '[dry-run] ' printf '%q ' "$@" printf '\n' else "$@" fi } run_remote() { local cmd="$1" if [[ "$DRY_RUN" == "1" ]]; then echo "[dry-run] ssh ${PROXMOX_USER}@${PROXMOX_HOST} $cmd" else ssh "${SSH_OPTS[@]}" "${PROXMOX_USER}@${PROXMOX_HOST}" "$cmd" fi } cleanup() { rm -f "$TMP_ARCHIVE" } trap cleanup EXIT while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=1 ;; --apply) APPLY=1 ;; --skip-create) SKIP_CREATE=1 ;; --skip-system-packages) SKIP_SYSTEM_PACKAGES=1 ;; -h|--help) usage; exit 0 ;; *) fail "Unknown argument: $1" ;; esac shift done if [[ "$DRY_RUN" == "0" && "$APPLY" == "0" ]]; then fail "Refusing to make changes without --apply. Use --dry-run to preview." fi build_archive_from_local() { local src="$1" [[ -d "$src" ]] || fail "Local source repo not found: $src" log "Packing local source from $src" tar -C "$src" \ --exclude='.git' \ --exclude='node_modules' \ --exclude='webapp/node_modules' \ --exclude='orchestrator/node_modules' \ --exclude='contracts/node_modules' \ --exclude='webapp/.next' \ --exclude='orchestrator/dist' \ --exclude='.env' \ --exclude='.env.local' \ --exclude='webapp/.env.local' \ -czf "$TMP_ARCHIVE" . } ensure_archive() { if [[ -n "$LOCAL_SRC" ]]; then build_archive_from_local "$LOCAL_SRC" return fi local scratch scratch="$(mktemp -d /tmp/currencicombo-src-XXXXXX)" trap 'rm -rf "$scratch"; cleanup' EXIT log "Cloning $REPO_URL#$REPO_BRANCH to build deploy archive" git clone --depth=1 --branch "$REPO_BRANCH" "$REPO_URL" "$scratch/repo" >/dev/null 2>&1 if [[ -n "$REPO_REF" ]]; then git -C "$scratch/repo" fetch --depth=1 origin "$REPO_REF" >/dev/null 2>&1 || true git -C "$scratch/repo" checkout "$REPO_REF" >/dev/null 2>&1 fi build_archive_from_local "$scratch/repo" } ensure_ct() { local exists_cmd="pct status ${VMID} >/dev/null 2>&1" if ssh "${SSH_OPTS[@]}" "${PROXMOX_USER}@${PROXMOX_HOST}" "$exists_cmd" >/dev/null 2>&1; then log "CT ${VMID} already exists on ${PROXMOX_HOST}" return fi [[ "$SKIP_CREATE" == "0" ]] || fail "CT ${VMID} does not exist and --skip-create was set" local create_cmd="pct create ${VMID} local:vztmpl/${CT_TEMPLATE} --storage ${CT_STORAGE} --hostname ${CT_HOSTNAME} --memory ${CT_MEMORY_MB} --cores ${CT_CORES} --rootfs ${CT_STORAGE}:${CT_ROOTFS_GB} --net0 name=eth0,bridge=vmbr0,tag=${CT_VLAN_TAG},ip=${CT_IP}/${CT_PREFIX},gw=${CT_GW},type=veth --unprivileged 1 --swap ${CT_SWAP_MB} --onboot 1 --timezone ${CT_TIMEZONE} --features nesting=1,keyctl=1" log "Creating CT ${VMID} (${CT_HOSTNAME})" run_remote "$create_cmd" } ensure_ct_running() { local status status="$(ssh "${SSH_OPTS[@]}" "${PROXMOX_USER}@${PROXMOX_HOST}" "pct status ${VMID} 2>/dev/null | awk '{print \$2}'" || true)" if [[ "$status" != "running" ]]; then log "Starting CT ${VMID}" run_remote "pct start ${VMID}" if [[ "$DRY_RUN" == "0" ]]; then sleep 10 fi else log "CT ${VMID} already running" fi } push_archive() { log "Uploading deploy archive to ${PROXMOX_HOST}" run_local scp "${SSH_OPTS[@]}" "$TMP_ARCHIVE" "${PROXMOX_USER}@${PROXMOX_HOST}:${REMOTE_ARCHIVE}" run_remote "pct push ${VMID} ${REMOTE_ARCHIVE} ${PUSH_ARCHIVE}" run_remote "rm -f ${REMOTE_ARCHIVE}" } run_ct_script() { local body body="$(cat <<'INNER' set -euo pipefail export DEBIAN_FRONTEND=noninteractive DEPLOY_ROOT='__DEPLOY_ROOT__' POSTGRES_DB='__POSTGRES_DB__' POSTGRES_USER='__POSTGRES_USER__' POSTGRES_PASSWORD='__POSTGRES_PASSWORD__' REDIS_URL='__REDIS_URL__' ORCH_PORT='__ORCH_PORT__' WEB_PORT='__WEB_PORT__' NEXTAUTH_URL='__NEXTAUTH_URL__' NEXT_PUBLIC_ORCH_URL='__NEXT_PUBLIC_ORCH_URL__' SESSION_SECRET='__SESSION_SECRET__' JWT_SECRET='__JWT_SECRET__' API_KEYS='__API_KEYS__' CT_NODE_ENV='__CT_NODE_ENV__' PUSH_ARCHIVE='__PUSH_ARCHIVE__' SKIP_SYSTEM_PACKAGES='__SKIP_SYSTEM_PACKAGES__' if [[ "$SKIP_SYSTEM_PACKAGES" != "1" ]]; then apt-get update -qq apt-get install -y -qq ca-certificates curl gnupg git rsync build-essential postgresql redis-server if ! command -v node >/dev/null 2>&1 || [[ "$(node -v 2>/dev/null || true)" != v20* ]]; then curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y -qq nodejs fi fi systemctl enable postgresql redis-server >/dev/null 2>&1 || true systemctl restart postgresql redis-server install -d -m 0755 "$DEPLOY_ROOT" rm -rf /tmp/currencicombo-incoming mkdir -p /tmp/currencicombo-incoming tar -xzf "$PUSH_ARCHIVE" -C /tmp/currencicombo-incoming rsync -a --delete /tmp/currencicombo-incoming/ "$DEPLOY_ROOT/" rm -rf /tmp/currencicombo-incoming "$PUSH_ARCHIVE" cat > "$DEPLOY_ROOT/orchestrator/.env" < "$DEPLOY_ROOT/webapp/.env.local" </dev/null cd "$DEPLOY_ROOT/orchestrator" npm ci RUN_MIGRATIONS=true npm run migrate npm run build cd "$DEPLOY_ROOT/webapp" npm ci npm run build cat > /etc/systemd/system/currencicombo-orchestrator.service < /etc/systemd/system/currencicombo-webapp.service </dev/null systemctl restart currencicombo-orchestrator currencicombo-webapp sleep 8 curl -fsS "http://127.0.0.1:${ORCH_PORT}/health" >/dev/null curl -fsS "http://127.0.0.1:${WEB_PORT}/" >/dev/null INNER )" body="${body//__DEPLOY_ROOT__/$DEPLOY_ROOT}" body="${body//__POSTGRES_DB__/$POSTGRES_DB}" body="${body//__POSTGRES_USER__/$POSTGRES_USER}" body="${body//__POSTGRES_PASSWORD__/$POSTGRES_PASSWORD}" body="${body//__REDIS_URL__/$REDIS_URL}" body="${body//__ORCH_PORT__/$ORCH_PORT}" body="${body//__WEB_PORT__/$WEB_PORT}" body="${body//__NEXTAUTH_URL__/$NEXTAUTH_URL}" body="${body//__NEXT_PUBLIC_ORCH_URL__/$NEXT_PUBLIC_ORCH_URL}" body="${body//__SESSION_SECRET__/$SESSION_SECRET}" body="${body//__JWT_SECRET__/$JWT_SECRET}" body="${body//__API_KEYS__/$API_KEYS}" body="${body//__CT_NODE_ENV__/$CT_NODE_ENV}" body="${body//__PUSH_ARCHIVE__/$PUSH_ARCHIVE}" body="${body//__SKIP_SYSTEM_PACKAGES__/$SKIP_SYSTEM_PACKAGES}" if [[ "$DRY_RUN" == "1" ]]; then echo "[dry-run] pct exec ${VMID} -- bash -lc ''" else ssh "${SSH_OPTS[@]}" "${PROXMOX_USER}@${PROXMOX_HOST}" "pct exec ${VMID} -- bash -lc $(printf '%q' "$body")" fi } verify_from_host() { local cmd="pct exec ${VMID} -- bash -lc 'curl -fsS http://127.0.0.1:${ORCH_PORT}/health >/dev/null && curl -fsS http://127.0.0.1:${WEB_PORT}/ >/dev/null && systemctl is-active currencicombo-orchestrator currencicombo-webapp'" run_remote "$cmd" } ensure_archive if [[ "$DRY_RUN" == "0" ]]; then [[ -s "$TMP_ARCHIVE" ]] || fail "Failed to create deploy archive" fi ensure_ct ensure_ct_running push_archive run_ct_script verify_from_host log "CurrenciCombo deploy complete: CT ${VMID} on ${PROXMOX_HOST} (${CT_IP})" log "Web: http://${CT_IP}:${WEB_PORT}/" log "API: http://${CT_IP}:${ORCH_PORT}/health"