fix(release): restart m.tkmind.cn portal tunnel after Portal deploy

Bundle memind-portal-tunnel.sh into the runtime artifact and kickstart
cn.tkmind.memind-portal-tunnel on 103 after Portal health checks so 105
nginx can reach 127.0.0.1:19081 following runtime releases.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-29 10:22:19 +08:00
parent 12a1247c1f
commit 2b344dfa60
2 changed files with 86 additions and 4 deletions
+14 -1
View File
@@ -181,8 +181,18 @@ async function writeMetadata() {
type: 'module',
};
await writeFile(path.join(runtimeRoot, 'package.json'), `${JSON.stringify(runtimePackageJson, null, 2)}\n`);
const tunnelScript = path.join(root, 'scripts', 'memind-portal-tunnel.sh');
const runtimeScriptsDir = path.join(runtimeRoot, 'scripts');
await fs.mkdir(runtimeScriptsDir, { recursive: true });
if (await exists(tunnelScript)) {
await fs.copyFile(tunnelScript, path.join(runtimeScriptsDir, 'memind-portal-tunnel.sh'));
await fs.chmod(path.join(runtimeScriptsDir, 'memind-portal-tunnel.sh'), 0o755);
} else {
throw new Error(`缺少隧道脚本: ${tunnelScript}`);
}
await writeFile(
path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'),
path.join(runtimeScriptsDir, 'run-memind-portal-prod.sh'),
[
'#!/usr/bin/env bash',
'set -euo pipefail',
@@ -242,6 +252,8 @@ async function writeMetadata() {
'Deployment and operations transport:',
' 105 fixed IP: 120.26.184.105',
' 103 / Studio fixed IP: 58.38.22.103',
' m.tkmind.cn: 105 nginx -> 127.0.0.1:19081 -> reverse SSH tunnel -> Portal :8081',
' scripts/memind-portal-tunnel.sh must stay in runtime; release restarts cn.tkmind.memind-portal-tunnel',
' Do not switch back to 10.10.* LAN paths unless explicitly required.',
'',
].join('\n'),
@@ -256,6 +268,7 @@ async function main() {
await copyNodeModules();
await writeMetadata();
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 0o755);
await fs.chmod(path.join(runtimeRoot, 'scripts', 'memind-portal-tunnel.sh'), 0o755);
console.log('');
console.log(`Portal runtime 已生成: ${runtimeRoot}`);
}
+72 -3
View File
@@ -10,6 +10,9 @@ BACKUP_DIR="${REMOTE_ROOT}/backups/memind"
ARCHIVE_DIR="${REMOTE_ROOT}/archives"
HEALTH_URL="${STUDIO_HEALTH_URL:-http://127.0.0.1:8081/api/status}"
PORTAL_LABEL="cn.tkmind.memind-portal"
PORTAL_TUNNEL_LABEL="cn.tkmind.memind-portal-tunnel"
MEMIND_PORTAL_TUNNEL_HOST="${MEMIND_PORTAL_TUNNEL_HOST:-ssh105-public}"
MEMIND_PORTAL_TUNNEL_REMOTE_PORT="${MEMIND_PORTAL_TUNNEL_REMOTE_PORT:-19081}"
LAUNCHD_GUI="gui/$(id -u)"
RELEASE_TS="$(date +%Y%m%d-%H%M%S)"
SHORT_SHA="$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo no-git)"
@@ -41,7 +44,8 @@ usage() {
4. 停止旧 Portal 服务
5. 用 runtime artifact 替换 live 目录,只继承持久目录
6. 启动 Portal,保持旧端口 8081
7. 健康检查通过后,旧源码目录移入 archive,不再保留可运行 live 源码
7. 健康检查通过后重启 m.tkmind.cn 反向隧道 (105:19081 -> 103:8081)
8. 旧源码目录移入 archive,不再保留可运行 live 源码
EOF
}
@@ -139,7 +143,7 @@ fi
verify_runtime_artifact() {
local missing=0
for required in server.mjs mindspace-sandbox-mcp.mjs dist package.json scripts/run-memind-portal-prod.sh; do
for required in server.mjs mindspace-sandbox-mcp.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/memind-portal-tunnel.sh; do
if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
missing=1
@@ -273,7 +277,7 @@ scp -q "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}" "${HOST}:${INCOMING_DIR
say "在 103 停旧服务并切换到无源码 runtime"
ssh -o BatchMode=yes "${HOST}" \
"RELEASE_ID='${RELEASE_ID}' APP_DIR='${APP_DIR}' INCOMING_DIR='${INCOMING_DIR}' BACKUP_DIR='${BACKUP_DIR}' ARCHIVE_DIR='${ARCHIVE_DIR}' HEALTH_URL='${HEALTH_URL}' PORTAL_LABEL='${PORTAL_LABEL}' LAUNCHD_GUI='${LAUNCHD_GUI}' /bin/bash" <<'REMOTE_SCRIPT'
"RELEASE_ID='${RELEASE_ID}' APP_DIR='${APP_DIR}' INCOMING_DIR='${INCOMING_DIR}' BACKUP_DIR='${BACKUP_DIR}' ARCHIVE_DIR='${ARCHIVE_DIR}' HEALTH_URL='${HEALTH_URL}' PORTAL_LABEL='${PORTAL_LABEL}' PORTAL_TUNNEL_LABEL='${PORTAL_TUNNEL_LABEL}' MEMIND_PORTAL_TUNNEL_HOST='${MEMIND_PORTAL_TUNNEL_HOST}' MEMIND_PORTAL_TUNNEL_REMOTE_PORT='${MEMIND_PORTAL_TUNNEL_REMOTE_PORT}' LAUNCHD_GUI='${LAUNCHD_GUI}' /bin/bash" <<'REMOTE_SCRIPT'
set -euo pipefail
RUNTIME_DIR="${APP_DIR}.runtime-${RELEASE_ID}"
@@ -297,6 +301,67 @@ say() {
printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
}
ensure_portal_tunnel() {
local tunnel_script="${APP_DIR}/scripts/memind-portal-tunnel.sh"
local tunnel_label="${PORTAL_TUNNEL_LABEL:-cn.tkmind.memind-portal-tunnel}"
local tunnel_host="${MEMIND_PORTAL_TUNNEL_HOST:-ssh105-public}"
local remote_port="${MEMIND_PORTAL_TUNNEL_REMOTE_PORT:-19081}"
[[ -x "${tunnel_script}" ]] || {
echo "missing tunnel script: ${tunnel_script}" >&2
return 1
}
cat > "${HOME}/Library/LaunchAgents/${tunnel_label}.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${tunnel_label}</string>
<key>ProgramArguments</key>
<array>
<string>${tunnel_script}</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>MEMIND_PORTAL_TUNNEL_HOST</key>
<string>${tunnel_host}</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${HOME}/Library/Logs/memind-portal-tunnel.log</string>
<key>StandardErrorPath</key>
<string>${HOME}/Library/Logs/memind-portal-tunnel.log</string>
</dict>
</plist>
EOF
launchctl bootout "${LAUNCHD_GUI}/${tunnel_label}" >/dev/null 2>&1 || true
launchctl bootstrap "${LAUNCHD_GUI}" "${HOME}/Library/LaunchAgents/${tunnel_label}.plist" >/dev/null 2>&1 || true
launchctl enable "${LAUNCHD_GUI}/${tunnel_label}" >/dev/null 2>&1 || true
launchctl kickstart -k "${LAUNCHD_GUI}/${tunnel_label}" >/dev/null 2>&1 || true
sleep 2
if ! launchctl print "${LAUNCHD_GUI}/${tunnel_label}" 2>/dev/null | grep -q 'state = running'; then
echo "portal tunnel failed to start (${tunnel_label})" >&2
return 1
fi
if command -v ssh >/dev/null 2>&1; then
tunnel_code="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "${tunnel_host}" \
"curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${remote_port}/api/status" 2>/dev/null || true)"
if [[ "${tunnel_code}" == "200" ]]; then
say "105 隧道端口 ${remote_port} 健康"
else
echo "warn: 105:${remote_port} 健康检查未通过 (code=${tunnel_code:-000});请确认 105 nginx upstream 指向 127.0.0.1:${remote_port}" >&2
fi
fi
}
rollback() {
if [[ -d "${OLD_LIVE_DIR}" && ! -d "${APP_DIR}" ]]; then
mv "${OLD_LIVE_DIR}" "${APP_DIR}"
@@ -306,6 +371,7 @@ rollback() {
if ! curl -sf "${HEALTH_URL}" >/dev/null 2>&1; then
nohup "${APP_DIR}/scripts/run-memind-portal-prod.sh" >> "${HOME}/Library/Logs/memind-portal.log" 2>&1 &
fi
ensure_portal_tunnel >/dev/null 2>&1 || true
}
trap 'rollback' ERR
@@ -435,6 +501,9 @@ if [[ "${portal_code}" != "200" ]]; then
exit 1
fi
say "重启 m.tkmind.cn 反向隧道"
ensure_portal_tunnel
say "检查 live 目录中不再保留源码树"
allowed_live_mjs=(
"${APP_DIR}/server.mjs"