Files
memind/scripts/release-mindspace-service-prod.sh
john 67d529c81e fix(mindspace): keep standalone service data paths under MindSpace root
Prevent Portal .env from overriding MindSpace storage/users roots, and
set launchd env plus runtime docs to use /Users/john/MindSpace paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 18:06:16 +08:00

390 lines
12 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
say() {
printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
}
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
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
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 service"
mkdir -p "${APP_DIR}/data/mindspace" "${APP_DIR}/users"
launchctl bootstrap "${LAUNCHD_GUI}" "${SERVICE_PLIST}" >/dev/null 2>&1 || true
launchctl enable "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null 2>&1 || true
launchctl kickstart -k "${LAUNCHD_GUI}/${SERVICE_LABEL}" >/dev/null 2>&1 || true
SERVICE_STARTED=1
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" ]]; then
break
fi
sleep 1
done
[[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8082/health || true)" == "200" ]]
[[ "$(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" ]]
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 -s 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}"