432 lines
13 KiB
Bash
432 lines
13 KiB
Bash
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||
HOST="${STUDIO_HOST:-58.38.22.103}"
|
||
REMOTE_USER_ROOT="${STUDIO_USER_ROOT:-/Users/john}"
|
||
SHARED_MEMIND_ROOT="${STUDIO_SHARED_MEMIND_ROOT:-/Users/john/Project/Memind}"
|
||
APP_DIR="${REMOTE_USER_ROOT}/MindSpace"
|
||
INCOMING_DIR="/Users/john/Project/incoming/mindspace-service-runtime"
|
||
BACKUP_DIR="/Users/john/Project/backups/mindspace-service"
|
||
ARCHIVE_DIR="/Users/john/Project/archives/mindspace-service"
|
||
HEALTH_URL="${MINDSPACE_SERVICE_HEALTH_URL:-http://127.0.0.1:8082/health}"
|
||
CONTRACT_URL="${MINDSPACE_SERVICE_CONTRACT_URL:-http://127.0.0.1:8082/mindspace/v1/contract}"
|
||
PORTAL_HEALTH_URL="${STUDIO_HEALTH_URL:-http://127.0.0.1:8081/api/status}"
|
||
SERVICE_LABEL="cn.tkmind.mindspace-service"
|
||
PORTAL_LABEL="cn.tkmind.memind-portal"
|
||
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)"
|
||
RELEASE_ID="${RELEASE_TS}-${SHORT_SHA}"
|
||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mindspace-service-release.XXXXXX")"
|
||
RUNTIME_ROOT="${ROOT}/.runtime/mindspace-service"
|
||
BUNDLE_PATH="${TMP_DIR}/mindspace-service-${RELEASE_ID}.tar.gz"
|
||
MANIFEST_PATH="${TMP_DIR}/mindspace-service-${RELEASE_ID}.manifest.txt"
|
||
SHA_PATH="${TMP_DIR}/mindspace-service-${RELEASE_ID}.sha256"
|
||
DRY_RUN=0
|
||
SKIP_TESTS=0
|
||
SKIP_BUILD=0
|
||
AUTO_YES=0
|
||
|
||
cleanup() {
|
||
rm -rf "${TMP_DIR}"
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法:
|
||
bash scripts/release-mindspace-service-prod.sh [--dry-run] [--skip-tests] [--skip-build] [--yes]
|
||
|
||
说明:
|
||
1. 本地构建 MindSpace service runtime artifact
|
||
2. 上传到 103
|
||
3. 103 备份当前 /Users/john/MindSpace(如存在)
|
||
4. 103 备份 /Users/john/Project/Memind/.env
|
||
5. 启动 /Users/john/MindSpace 独立服务(127.0.0.1:8082)
|
||
6. 将 Memind 切到 remote adapter 并重启 Portal
|
||
7. 失败时恢复旧 MindSpace 服务目录与 Memind .env
|
||
EOF
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--dry-run) DRY_RUN=1 ;;
|
||
--skip-tests) SKIP_TESTS=1 ;;
|
||
--skip-build) SKIP_BUILD=1 ;;
|
||
--yes|-y) AUTO_YES=1 ;;
|
||
-h|--help)
|
||
usage
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo "未知参数: $1" >&2
|
||
usage >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
say() {
|
||
printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*"
|
||
}
|
||
|
||
need_cmd() {
|
||
command -v "$1" >/dev/null 2>&1 || {
|
||
echo "缺少命令: $1" >&2
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
need_cmd ssh
|
||
need_cmd scp
|
||
need_cmd tar
|
||
need_cmd shasum
|
||
|
||
if [[ -x "${ROOT}/scripts/check-release-ready.sh" ]]; then
|
||
bash "${ROOT}/scripts/check-release-ready.sh"
|
||
else
|
||
echo "缺少发布前闸门: ${ROOT}/scripts/check-release-ready.sh" >&2
|
||
exit 1
|
||
fi
|
||
|
||
say "本地预检查"
|
||
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo mindspace-service-ssh-ok" >/dev/null
|
||
ssh -o BatchMode=yes "${HOST}" "test -d '${SHARED_MEMIND_ROOT}' && test -f '${SHARED_MEMIND_ROOT}/.env'" >/dev/null
|
||
|
||
if [[ "${SKIP_TESTS}" -ne 1 ]]; then
|
||
say "运行最小验证"
|
||
(
|
||
cd "${ROOT}"
|
||
node --test mindspace-service/mindspace-rpc-server.test.mjs >/dev/null
|
||
)
|
||
fi
|
||
|
||
if [[ "${SKIP_BUILD}" -ne 1 ]]; then
|
||
say "构建 MindSpace service runtime"
|
||
(
|
||
cd "${ROOT}"
|
||
node scripts/build-mindspace-service-runtime.mjs
|
||
)
|
||
fi
|
||
|
||
[[ -d "${RUNTIME_ROOT}" ]] || {
|
||
echo "缺少 runtime 目录: ${RUNTIME_ROOT}" >&2
|
||
exit 1
|
||
}
|
||
|
||
verify_runtime_artifact() {
|
||
local missing=0
|
||
for required in README.md package.json server.mjs mindspace-rpc-server.mjs mindspace-service-bootstrap.mjs scripts/run-mindspace-prod.sh memind-source RUNBOOK.txt; do
|
||
if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
|
||
echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
|
||
missing=1
|
||
fi
|
||
done
|
||
if [[ "${missing}" -ne 0 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
verify_runtime_artifact
|
||
|
||
say "生成发布清单"
|
||
cat > "${MANIFEST_PATH}" <<EOF
|
||
release_id=${RELEASE_ID}
|
||
created_at=$(date '+%Y-%m-%d %H:%M:%S %z')
|
||
host=$(hostname)
|
||
git_head=$(git -C "${ROOT}" rev-parse HEAD)
|
||
git_branch=$(git -C "${ROOT}" branch --show-current)
|
||
artifact=.runtime/mindspace-service
|
||
service_root=${APP_DIR}
|
||
shared_memind_root=${SHARED_MEMIND_ROOT}
|
||
service_port=8082
|
||
EOF
|
||
|
||
say "打包 MindSpace service runtime artifact"
|
||
COPYFILE_DISABLE=1 tar -czf "${BUNDLE_PATH}" -C "${RUNTIME_ROOT}" .
|
||
shasum -a 256 "${BUNDLE_PATH}" > "${SHA_PATH}"
|
||
|
||
if [[ "${DRY_RUN}" -eq 1 ]]; then
|
||
say "dry-run 完成"
|
||
ls -lh "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}"
|
||
exit 0
|
||
fi
|
||
|
||
if [[ "${AUTO_YES}" -ne 1 ]]; then
|
||
echo ""
|
||
echo "即将部署 MindSpace service 到 ${HOST}:${APP_DIR}"
|
||
echo "并把 Memind 切换到 remote adapter -> http://127.0.0.1:8082"
|
||
read -r -p "确认继续?[y/N] " reply
|
||
if [[ ! "${reply}" =~ ^[Yy]$ ]]; then
|
||
echo "已取消"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
say "上传 MindSpace service runtime 到 103"
|
||
ssh "${HOST}" "mkdir -p '${INCOMING_DIR}' '${BACKUP_DIR}' '${ARCHIVE_DIR}'"
|
||
scp -q "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${HOST}:${INCOMING_DIR}/"
|
||
|
||
say "在 103 切换 MindSpace service 并更新 Memind remote adapter"
|
||
ssh -o BatchMode=yes "${HOST}" 'bash -s' -- "${RELEASE_ID}" <<'REMOTE'
|
||
set -euo pipefail
|
||
|
||
RELEASE_ID="$1"
|
||
USER_ROOT="/Users/john"
|
||
APP_DIR="${USER_ROOT}/MindSpace"
|
||
NEW_DIR="${USER_ROOT}/MindSpace.release-${RELEASE_ID}"
|
||
INCOMING_DIR="/Users/john/Project/incoming/mindspace-service-runtime"
|
||
BACKUP_DIR="/Users/john/Project/backups/mindspace-service"
|
||
ARCHIVE_DIR="/Users/john/Project/archives/mindspace-service"
|
||
SHARED_MEMIND_ROOT="/Users/john/Project/Memind"
|
||
BUNDLE="${INCOMING_DIR}/mindspace-service-${RELEASE_ID}.tar.gz"
|
||
MANIFEST="${INCOMING_DIR}/mindspace-service-${RELEASE_ID}.manifest.txt"
|
||
ENV_FILE="${SHARED_MEMIND_ROOT}/.env"
|
||
ENV_BACKUP="${BACKUP_DIR}/memind-env-${RELEASE_ID}.before"
|
||
APP_BACKUP="${BACKUP_DIR}/mindspace-service-${RELEASE_ID}.before.tar.gz"
|
||
OLD_APP_ARCHIVE="${ARCHIVE_DIR}/MindSpace-before-${RELEASE_ID}"
|
||
SERVICE_LABEL="cn.tkmind.mindspace-service"
|
||
PORTAL_LABEL="cn.tkmind.memind-portal"
|
||
LAUNCHD_GUI="gui/$(id -u)"
|
||
SERVICE_PLIST="${HOME}/Library/LaunchAgents/${SERVICE_LABEL}.plist"
|
||
SERVICE_PLIST_BAK="${BACKUP_DIR}/${SERVICE_LABEL}.${RELEASE_ID}.plist.bak"
|
||
SERVICE_STARTED=0
|
||
PORTAL_TOUCHED=0
|
||
APP_MOVED=0
|
||
HAD_OLD_APP=0
|
||
PERSISTED_DATA_MOVED=0
|
||
PERSISTED_USERS_MOVED=0
|
||
|
||
say() {
|
||
printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
|
||
}
|
||
|
||
service_is_running() {
|
||
launchctl print "${LAUNCHD_GUI}/${SERVICE_LABEL}" 2>/dev/null | grep -q 'state = running'
|
||
}
|
||
|
||
rollback() {
|
||
set +e
|
||
say "rollback: restoring previous state"
|
||
if [[ "${PORTAL_TOUCHED}" == "1" && -f "${ENV_BACKUP}" ]]; then
|
||
cp "${ENV_BACKUP}" "${ENV_FILE}"
|
||
launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || true
|
||
fi
|
||
if [[ "${SERVICE_STARTED}" == "1" ]]; then
|
||
launchctl bootout "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null 2>&1 || true
|
||
fi
|
||
if [[ -f "${SERVICE_PLIST_BAK}" ]]; then
|
||
cp "${SERVICE_PLIST_BAK}" "${SERVICE_PLIST}"
|
||
else
|
||
rm -f "${SERVICE_PLIST}"
|
||
fi
|
||
if [[ "${APP_MOVED}" == "1" ]]; then
|
||
if [[ "${HAD_OLD_APP}" == "1" ]]; then
|
||
if [[ "${PERSISTED_DATA_MOVED}" == "1" && ( -e "${APP_DIR}/data" || -L "${APP_DIR}/data" ) ]]; then
|
||
rm -rf "${OLD_APP_ARCHIVE}/data"
|
||
mv "${APP_DIR}/data" "${OLD_APP_ARCHIVE}/data"
|
||
fi
|
||
if [[ "${PERSISTED_USERS_MOVED}" == "1" && ( -e "${APP_DIR}/users" || -L "${APP_DIR}/users" ) ]]; then
|
||
rm -rf "${OLD_APP_ARCHIVE}/users"
|
||
mv "${APP_DIR}/users" "${OLD_APP_ARCHIVE}/users"
|
||
fi
|
||
fi
|
||
rm -rf "${APP_DIR}"
|
||
if [[ "${HAD_OLD_APP}" == "1" && -d "${OLD_APP_ARCHIVE}" ]]; then
|
||
mv "${OLD_APP_ARCHIVE}" "${APP_DIR}"
|
||
fi
|
||
fi
|
||
if [[ "${HAD_OLD_APP}" == "1" && -f "${SERVICE_PLIST}" ]]; then
|
||
launchctl bootstrap "${LAUNCHD_GUI}" "${SERVICE_PLIST}" >/dev/null 2>&1 || true
|
||
launchctl kickstart -k "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null 2>&1 || true
|
||
fi
|
||
exit 1
|
||
}
|
||
|
||
trap rollback ERR
|
||
|
||
[[ -f "${BUNDLE}" && -f "${MANIFEST}" ]]
|
||
|
||
say "校验产物完整性"
|
||
cd "${INCOMING_DIR}"
|
||
tar -tzf "${BUNDLE}" >/dev/null
|
||
|
||
say "备份 Memind .env"
|
||
cp "${ENV_FILE}" "${ENV_BACKUP}"
|
||
|
||
if [[ -d "${APP_DIR}" ]]; then
|
||
HAD_OLD_APP=1
|
||
say "备份旧 MindSpace service 目录"
|
||
COPYFILE_DISABLE=1 tar -czf "${APP_BACKUP}" -C "$(dirname "${APP_DIR}")" "$(basename "${APP_DIR}")"
|
||
fi
|
||
|
||
say "准备新的 runtime 目录"
|
||
rm -rf "${NEW_DIR}"
|
||
mkdir -p "${NEW_DIR}"
|
||
tar -xzf "${BUNDLE}" -C "${NEW_DIR}"
|
||
cp "${MANIFEST}" "${NEW_DIR}/.release-manifest.txt"
|
||
|
||
say "更新 MindSpace service LaunchAgent"
|
||
if [[ -f "${SERVICE_PLIST}" ]]; then
|
||
cp "${SERVICE_PLIST}" "${SERVICE_PLIST_BAK}"
|
||
fi
|
||
cat > "${SERVICE_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>${SERVICE_LABEL}</string>
|
||
<key>ProgramArguments</key>
|
||
<array>
|
||
<string>${APP_DIR}/scripts/run-mindspace-prod.sh</string>
|
||
</array>
|
||
<key>WorkingDirectory</key>
|
||
<string>${APP_DIR}</string>
|
||
<key>EnvironmentVariables</key>
|
||
<dict>
|
||
<key>PATH</key>
|
||
<string>/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin</string>
|
||
<key>MEMIND_SHARED_ROOT</key>
|
||
<string>${SHARED_MEMIND_ROOT}</string>
|
||
<key>MINDSPACE_SERVICE_PORT</key>
|
||
<string>8082</string>
|
||
<key>MINDSPACE_SERVICE_HOST</key>
|
||
<string>127.0.0.1</string>
|
||
<key>MINDSPACE_STORAGE_ROOT</key>
|
||
<string>${APP_DIR}/data/mindspace</string>
|
||
<key>H5_USERS_ROOT</key>
|
||
<string>${APP_DIR}/users</string>
|
||
<key>MINDSPACE_SERVICE_AGENT_WORKER_ENABLED</key>
|
||
<string>true</string>
|
||
<key>MINDSPACE_SERVICE_WORKSPACE_MAINTENANCE</key>
|
||
<string>true</string>
|
||
</dict>
|
||
<key>RunAtLoad</key>
|
||
<true/>
|
||
<key>KeepAlive</key>
|
||
<true/>
|
||
<key>StandardOutPath</key>
|
||
<string>${HOME}/Library/Logs/mindspace-service.log</string>
|
||
<key>StandardErrorPath</key>
|
||
<string>${HOME}/Library/Logs/mindspace-service.log</string>
|
||
</dict>
|
||
</plist>
|
||
EOF
|
||
|
||
say "切换 MindSpace service live 目录"
|
||
launchctl bootout "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null 2>&1 || true
|
||
for _ in $(seq 1 60); do
|
||
if ! lsof -nP -iTCP:8082 -sTCP:LISTEN >/dev/null 2>&1; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
if lsof -nP -iTCP:8082 -sTCP:LISTEN >/dev/null 2>&1; then
|
||
echo "旧 MindSpace service 未在超时内退出,拒绝切换 runtime" >&2
|
||
exit 1
|
||
fi
|
||
rm -rf "${OLD_APP_ARCHIVE}"
|
||
if [[ -d "${APP_DIR}" ]]; then
|
||
mv "${APP_DIR}" "${OLD_APP_ARCHIVE}"
|
||
fi
|
||
mv "${NEW_DIR}" "${APP_DIR}"
|
||
APP_MOVED=1
|
||
|
||
say "继承 MindSpace 持久目录"
|
||
if [[ "${HAD_OLD_APP}" == "1" && ( -e "${OLD_APP_ARCHIVE}/data" || -L "${OLD_APP_ARCHIVE}/data" ) ]]; then
|
||
rm -rf "${APP_DIR}/data"
|
||
mv "${OLD_APP_ARCHIVE}/data" "${APP_DIR}/data"
|
||
PERSISTED_DATA_MOVED=1
|
||
fi
|
||
if [[ "${HAD_OLD_APP}" == "1" && ( -e "${OLD_APP_ARCHIVE}/users" || -L "${OLD_APP_ARCHIVE}/users" ) ]]; then
|
||
rm -rf "${APP_DIR}/users"
|
||
mv "${OLD_APP_ARCHIVE}/users" "${APP_DIR}/users"
|
||
PERSISTED_USERS_MOVED=1
|
||
fi
|
||
|
||
say "启动 MindSpace service"
|
||
mkdir -p "${APP_DIR}/data/mindspace" "${APP_DIR}/users"
|
||
launchctl bootstrap "${LAUNCHD_GUI}" "${SERVICE_PLIST}" >/dev/null
|
||
SERVICE_STARTED=1
|
||
launchctl enable "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null
|
||
launchctl kickstart -k "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null
|
||
|
||
for _ in $(seq 1 30); do
|
||
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/health || true)"
|
||
if [[ "${code}" == "200" ]] && service_is_running; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/health || true)" == "200" ]]
|
||
service_is_running
|
||
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/mindspace/v1/contract || true)" == "200" ]]
|
||
|
||
set_env() {
|
||
local key="$1"
|
||
local value="$2"
|
||
local file="$3"
|
||
local tmp
|
||
tmp="$(mktemp "${TMPDIR:-/tmp}/env.XXXXXX")"
|
||
awk -v k="${key}" -v v="${value}" '
|
||
BEGIN { done = 0 }
|
||
index($0, k "=") == 1 {
|
||
if (!done) {
|
||
print k "=" v
|
||
done = 1
|
||
}
|
||
next
|
||
}
|
||
{ print }
|
||
END {
|
||
if (!done) print k "=" v
|
||
}
|
||
' "${file}" > "${tmp}"
|
||
mv "${tmp}" "${file}"
|
||
}
|
||
|
||
TOKEN="$(grep -E '^TKMIND_SERVER__SECRET_KEY=' "${ENV_FILE}" | tail -1 | cut -d= -f2- || true)"
|
||
if [[ -z "${TOKEN}" ]]; then
|
||
echo "Memind .env 缺少 TKMIND_SERVER__SECRET_KEY,无法写入 remote adapter token" >&2
|
||
exit 1
|
||
fi
|
||
|
||
say "切换 Memind 到 remote adapter"
|
||
set_env MINDSPACE_SERVER_ADAPTER remote "${ENV_FILE}"
|
||
set_env MINDSPACE_REMOTE_BASE_URL http://127.0.0.1:8082 "${ENV_FILE}"
|
||
set_env MINDSPACE_REMOTE_AUTH_TOKEN "${TOKEN}" "${ENV_FILE}"
|
||
set_env MEMIND_WORKSPACE_MAINTENANCE 0 "${ENV_FILE}"
|
||
PORTAL_TOUCHED=1
|
||
launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || true
|
||
|
||
for _ in $(seq 1 30); do
|
||
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8081/api/status || true)"
|
||
if [[ "${code}" == "200" ]]; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8081/api/status || true)" == "200" ]]
|
||
sleep 3
|
||
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/health || true)" == "200" ]]
|
||
service_is_running
|
||
|
||
trap - ERR
|
||
|
||
say "切换完成"
|
||
printf 'release_id=%s\n' "${RELEASE_ID}"
|
||
printf 'service_root=%s\n' "${APP_DIR}"
|
||
printf 'service_backup=%s\n' "${APP_BACKUP}"
|
||
printf 'env_backup=%s\n' "${ENV_BACKUP}"
|
||
printf 'service_health=%s\n' "$(curl -fsS http://127.0.0.1:8082/health)"
|
||
REMOTE
|
||
|
||
say "发布完成"
|
||
printf 'release_id=%s\n' "${RELEASE_ID}"
|
||
printf 'bundle=%s\n' "${BUNDLE_PATH}"
|
||
printf 'manifest=%s\n' "${MANIFEST_PATH}"
|
||
printf 'sha=%s\n' "${SHA_PATH}"
|