#!/usr/bin/env bash # Deploy the Hub to a host: rsync the repo, install deps, build, restart service. # # Configure these env vars (e.g. in CI secrets): # PLATFORM_DEPLOY_HOST required # PLATFORM_DEPLOY_SSH_KEY required (private key for the deploy user) # PLATFORM_DEPLOY_USER optional, defaults to deploy # PLATFORM_DEPLOY_PORT optional SSH port, defaults to 22 # PLATFORM_DEPLOY_HUB_PORT required Silo listener port # PLATFORM_DEPLOY_BASE optional, defaults to /srv/curriculum-project-hub # PLATFORM_DEPLOY_RELEASE optional immutable release id, defaults to git HEAD # PLATFORM_DEPLOY_INSTANCE required, Silo instance id # PLATFORM_DEPLOY_WORKSPACE_ROOT required short per-Silo path (for example /w/997) # PLATFORM_DEPLOY_MEMORY_MAX / CPU_QUOTA / TASKS_MAX required ceilings # PLATFORM_DEPLOY_HEALTH_URL optional, defaults to http://127.0.0.1:8788/api/healthz # The target host must have Node.js 24+, npm, rsync, PostgreSQL client tools # (`pg_isready`), systemd, bubblewrap (`bwrap`) and `socat` (for the ADR-0018 agent sandbox), # and a compatible `cph` binary named by platform.env. The service installer # provisions the dedicated non-root identity and persistent directories. # Use a dedicated `deploy` user that has passwordless sudo for systemctl and # writing the named /etc/systemd/system/cph-hub-.service through the installer. set -euo pipefail HOST="${PLATFORM_DEPLOY_HOST:?PLATFORM_DEPLOY_HOST required}" SSH_KEY="${PLATFORM_DEPLOY_SSH_KEY:?PLATFORM_DEPLOY_SSH_KEY required}" DEPLOY_USER="${PLATFORM_DEPLOY_USER:-deploy}" PORT="${PLATFORM_DEPLOY_PORT:-22}" HUB_PORT="${PLATFORM_DEPLOY_HUB_PORT:?PLATFORM_DEPLOY_HUB_PORT required}" BASE="${PLATFORM_DEPLOY_BASE:-/srv/curriculum-project-hub}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" RELEASE_ID="${PLATFORM_DEPLOY_RELEASE:-$(git -C "$REPO_ROOT" rev-parse --verify HEAD)}" [[ "$RELEASE_ID" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "invalid PLATFORM_DEPLOY_RELEASE" >&2; exit 1; } INSTANCE_ID="${PLATFORM_DEPLOY_INSTANCE:?PLATFORM_DEPLOY_INSTANCE required}" WORKSPACE_ROOT="${PLATFORM_DEPLOY_WORKSPACE_ROOT:?PLATFORM_DEPLOY_WORKSPACE_ROOT required}" SERVICE_UNIT="cph-hub-$INSTANCE_ID.service" MEMORY_MAX="${PLATFORM_DEPLOY_MEMORY_MAX:?PLATFORM_DEPLOY_MEMORY_MAX required}" CPU_QUOTA="${PLATFORM_DEPLOY_CPU_QUOTA:?PLATFORM_DEPLOY_CPU_QUOTA required}" TASKS_MAX="${PLATFORM_DEPLOY_TASKS_MAX:?PLATFORM_DEPLOY_TASKS_MAX required}" HEALTH_URL="${PLATFORM_DEPLOY_HEALTH_URL:-http://127.0.0.1:$HUB_PORT/api/healthz}" HUB_DIR="$BASE/releases/$RELEASE_ID/hub" RELEASE_DIR="$BASE/releases/$RELEASE_ID" SSH_OPTS=(-i "$SSH_KEY" -p "$PORT" -o StrictHostKeyChecking=accept-new) release_ready=false if ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" "test -f '$RELEASE_DIR/.complete'"; then release_ready=true elif ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" "test -e '$RELEASE_DIR'"; then echo "incomplete release already exists: $RELEASE_DIR" >&2 exit 1 else ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" "mkdir -p '$HUB_DIR'" fi if [ "$release_ready" = false ]; then # 1. Populate a new release directory. A completed release is never mutated. rsync -az --delete \ --exclude node_modules --exclude dist --exclude .env \ -e "ssh ${SSH_OPTS[*]}" \ "$REPO_ROOT/hub/" "$DEPLOY_USER@$HOST:$HUB_DIR/" # 2. Install deps (hub + admin-web), audit hub prod, build tsc + SPA, mark complete. # `npm run build` → tsc then admin:build → admin-web/build for registerStaticSpa. ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" \ "cd '$HUB_DIR' && npm ci && npm ci --prefix admin-web && npm run audit:production && npm run build && touch '$RELEASE_DIR/.complete'" fi # 3. Ensure the service is installed (idempotent), then restart. ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" " set -euo pipefail if [ \"\$(id -u)\" = \"0\" ]; then BASE='$BASE' HUB_DIR='$HUB_DIR' INSTANCE_ID='$INSTANCE_ID' WORKSPACE_ROOT='$WORKSPACE_ROOT' PORT='$HUB_PORT' MEMORY_MAX='$MEMORY_MAX' CPU_QUOTA='$CPU_QUOTA' TASKS_MAX='$TASKS_MAX' bash '$HUB_DIR/deploy/install_service.sh' systemctl restart '$SERVICE_UNIT' systemctl is-active --quiet '$SERVICE_UNIT' else sudo -n BASE='$BASE' HUB_DIR='$HUB_DIR' INSTANCE_ID='$INSTANCE_ID' WORKSPACE_ROOT='$WORKSPACE_ROOT' PORT='$HUB_PORT' MEMORY_MAX='$MEMORY_MAX' CPU_QUOTA='$CPU_QUOTA' TASKS_MAX='$TASKS_MAX' bash '$HUB_DIR/deploy/install_service.sh' sudo -n systemctl restart '$SERVICE_UNIT' sudo -n systemctl is-active --quiet '$SERVICE_UNIT' fi for attempt in \$(seq 1 30); do if curl --fail --silent '$HEALTH_URL' >/dev/null 2>&1; then exit 0 fi sleep 1 done curl --fail --silent --show-error '$HEALTH_URL' >/dev/null " echo "Deployed cph-hub to $HOST ($HUB_DIR)"