forked from bai/curriculum-project-hub
6990082247
三处引用旧工程名/旧资源路径的地方一并更新,它们必须同时改 —— 少改一处 就是静默故障,而不是构建期报错: 1. 部署脚本(deploy_platform.sh / deploy_fleet_release.sh):npm ci 的 prefix、rsync 排除项、构建产物存在性检查从 database-admin 换成 filelib-web。最后一项是真门禁:static.ts 缺产物时只 warn 不注册路由, 漏改会让 /app 与 /database 静默 404 —— 恰是 database-admin 长期处于 禁用状态的原因。 2. silo 限流豁免:资源路径随 appDir 改名而变(/database/_app/* 已不存在, 现为 /_filelib/*);/app/* 此前不在豁免列表,它现在也是 SPA 外壳, 客户端路由无法预先枚举。 注:/database/* 是整体豁免,filelib 的 JSON API 也绕过限流预算。这是 迁移前就有的行为,原样保留,但覆盖面因多了 /app/* 而变宽。 3. 回归测试:把 registerStaticSpa 与 registerDatabaseSpa 挂到同一个 Fastify 实例,断言 ready() 不因重复路由抛错 —— appDir 若用回默认的 _app,这里会红(ADR-0029 的承重约束)。另断言 /app 与 /database/dashboard/users 返回同一份字节(SPA 回退不读请求)、body 含 /_filelib/。构建产物缺失时不 skip 而是直接失败:那说明该先跑 filelib-web 的 build,不是测试不适用。
92 lines
4.8 KiB
Bash
Executable File
92 lines
4.8 KiB
Bash
Executable File
#!/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-<instance>.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 + both SPAs), audit hub prod, build tsc + SPAs, mark complete.
|
|
# `npm run build` → tsc then admin:build + filelib:build → admin-web/build and
|
|
# filelib-web/build for registerStaticSpa / registerDatabaseSpa.
|
|
ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" \
|
|
"cd '$HUB_DIR' && PUPPETEER_SKIP_DOWNLOAD=1 npm ci && npm ci --prefix admin-web && npm ci --prefix filelib-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)"
|