255 lines
7.8 KiB
Bash
255 lines
7.8 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
HOST="${H5_DEPLOY_HOST:-root@120.26.184.105}"
|
|
REMOTE_ROOT="${REMOTE_ROOT:-/root/tkmind_go}"
|
|
APP_DIR="${APP_DIR:-${REMOTE_ROOT}/ui/h5}"
|
|
INCOMING_DIR="${INCOMING_DIR:-${REMOTE_ROOT}/incoming/memind-portal-runtime-105}"
|
|
BACKUP_DIR="${BACKUP_DIR:-${REMOTE_ROOT}/backups}"
|
|
ARCHIVE_DIR="${ARCHIVE_DIR:-${REMOTE_ROOT}/archives}"
|
|
SERVICE_NAME="${SERVICE_NAME:-goose-h5}"
|
|
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:8080/api/status}"
|
|
RUNTIME_ROOT="${ROOT}/.runtime/portal"
|
|
RELEASE_ID="${RELEASE_ID:-$(date +%Y%m%d-%H%M%S)}"
|
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/memind-portal-runtime-105.XXXXXX")"
|
|
BUNDLE_PATH="${TMP_DIR}/memind-portal-runtime-105-${RELEASE_ID}.tar.gz"
|
|
MANIFEST_PATH="${TMP_DIR}/memind-portal-runtime-105-${RELEASE_ID}.manifest.txt"
|
|
SHA_PATH="${TMP_DIR}/memind-portal-runtime-105-${RELEASE_ID}.sha256"
|
|
DRY_RUN=0
|
|
SKIP_BUILD=0
|
|
SKIP_TESTS=0
|
|
AUTO_YES=0
|
|
|
|
cleanup() {
|
|
rm -rf "${TMP_DIR}"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
bash scripts/release-portal-runtime-105.sh [--dry-run] [--skip-build] [--skip-tests] [--yes]
|
|
|
|
Behavior:
|
|
1. Optionally runs targeted local tests
|
|
2. Builds the local Portal runtime artifact
|
|
3. Packages .runtime/portal into a tarball
|
|
4. Uploads the tarball to 105
|
|
5. Replaces the 105 live runtime while preserving config and persisted data
|
|
|
|
Preserved on 105:
|
|
.env, MindSpace/, data/, users/, logs/, public/plaza-covers/
|
|
EOF
|
|
}
|
|
|
|
say() {
|
|
printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dry-run)
|
|
DRY_RUN=1
|
|
;;
|
|
--skip-build)
|
|
SKIP_BUILD=1
|
|
;;
|
|
--skip-tests)
|
|
SKIP_TESTS=1
|
|
;;
|
|
--yes)
|
|
AUTO_YES=1
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
cd "${ROOT}"
|
|
|
|
say "检查 105 SSH"
|
|
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo portal-runtime-105-ssh-ok" >/dev/null
|
|
|
|
if [[ "${SKIP_TESTS}" -ne 1 ]]; then
|
|
say "运行微信相关本地测试"
|
|
node --test wechat-mp.test.mjs wechat-oauth.test.mjs wechat-pay.test.mjs user-auth.test.mjs capabilities.test.mjs llm-providers.test.mjs
|
|
fi
|
|
|
|
if [[ "${SKIP_BUILD}" -ne 1 ]]; then
|
|
say "构建 Portal runtime"
|
|
PORTAL_RUNTIME_NODE_TARGET=node22 PORTAL_RUNTIME_INSTALL_MODE=remote-npm-install \
|
|
node scripts/build-portal-runtime.mjs --skip-node-modules
|
|
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 scripts/load-env.mjs scripts/wechat-mp-menu.mjs; do
|
|
if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
|
|
echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
|
|
missing=1
|
|
fi
|
|
done
|
|
if [[ "${missing}" -ne 0 ]]; then
|
|
echo "请先执行: node scripts/build-portal-runtime.mjs" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
verify_runtime_artifact
|
|
|
|
if [[ "${AUTO_YES}" -ne 1 && "${DRY_RUN}" -ne 1 ]]; then
|
|
say "发布确认"
|
|
echo "目标主机: ${HOST}"
|
|
echo "目标目录: ${APP_DIR}"
|
|
echo "systemd 服务: ${SERVICE_NAME}"
|
|
echo "发布编号: ${RELEASE_ID}"
|
|
echo "本地 HEAD: $(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
read -r -p "确认继续发布到 105? [y/N] " confirm </dev/tty
|
|
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 0
|
|
fi
|
|
|
|
say "生成发布清单"
|
|
{
|
|
echo "release_id=${RELEASE_ID}"
|
|
echo "created_at=$(date '+%Y-%m-%d %H:%M:%S %z')"
|
|
echo "host=$(hostname)"
|
|
echo "git_head=$(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
echo "git_branch=$(git -C "${ROOT}" branch --show-current 2>/dev/null || echo detached)"
|
|
echo "artifact=.runtime/portal"
|
|
echo "target=105"
|
|
echo "persisted_items=.env, MindSpace, data, users, logs, public/plaza-covers"
|
|
} > "${MANIFEST_PATH}"
|
|
|
|
say "打包 Portal runtime artifact"
|
|
tar -czf "${BUNDLE_PATH}" -C "${RUNTIME_ROOT}" .
|
|
checksum="$(shasum -a 256 "${BUNDLE_PATH}" | awk '{print $1}')"
|
|
printf '%s %s\n' "${checksum}" "$(basename "${BUNDLE_PATH}")" > "${SHA_PATH}"
|
|
|
|
if [[ "${DRY_RUN}" -eq 1 ]]; then
|
|
say "dry-run 完成"
|
|
ls -lh "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}"
|
|
exit 0
|
|
fi
|
|
|
|
say "上传 Portal runtime 到 105"
|
|
ssh -o BatchMode=yes "${HOST}" "mkdir -p '${INCOMING_DIR}' '${BACKUP_DIR}' '${ARCHIVE_DIR}'"
|
|
scp -q "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}" "${HOST}:${INCOMING_DIR}/"
|
|
|
|
say "在 105 切换到新的 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}' SERVICE_NAME='${SERVICE_NAME}' /bin/bash" <<'REMOTE_SCRIPT'
|
|
set -euo pipefail
|
|
|
|
RUNTIME_DIR="${APP_DIR}.runtime-${RELEASE_ID}"
|
|
OLD_LIVE_DIR="${ARCHIVE_DIR}/h5-before-${RELEASE_ID}"
|
|
BUNDLE="${INCOMING_DIR}/memind-portal-runtime-105-${RELEASE_ID}.tar.gz"
|
|
MANIFEST="${INCOMING_DIR}/memind-portal-runtime-105-${RELEASE_ID}.manifest.txt"
|
|
SHA_FILE="${INCOMING_DIR}/memind-portal-runtime-105-${RELEASE_ID}.sha256"
|
|
FULL_BACKUP_TAR="${BACKUP_DIR}/h5-full-${RELEASE_ID}-before.tar.gz"
|
|
PERSIST_BACKUP_TAR="${BACKUP_DIR}/h5-persisted-${RELEASE_ID}-before.tar.gz"
|
|
PERSISTED_ITEMS=(
|
|
".env"
|
|
"MindSpace"
|
|
"data"
|
|
"users"
|
|
"logs"
|
|
"public/plaza-covers"
|
|
)
|
|
|
|
say() {
|
|
printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
|
|
}
|
|
|
|
rollback() {
|
|
if [[ -d "${OLD_LIVE_DIR}" && ! -d "${APP_DIR}" ]]; then
|
|
mv "${OLD_LIVE_DIR}" "${APP_DIR}"
|
|
fi
|
|
systemctl restart "${SERVICE_NAME}" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
trap 'rollback' ERR
|
|
|
|
[[ -f "${BUNDLE}" ]] || { echo "missing bundle: ${BUNDLE}" >&2; exit 1; }
|
|
[[ -f "${MANIFEST}" ]] || { echo "missing manifest: ${MANIFEST}" >&2; exit 1; }
|
|
[[ -f "${SHA_FILE}" ]] || { echo "missing sha file: ${SHA_FILE}" >&2; exit 1; }
|
|
[[ -f "${APP_DIR}/.env" ]] || { echo "missing required config: ${APP_DIR}/.env" >&2; exit 1; }
|
|
|
|
say "校验产物完整性"
|
|
cd "${INCOMING_DIR}"
|
|
shasum -a 256 -c "$(basename "${SHA_FILE}")"
|
|
|
|
say "备份当前 live 全目录"
|
|
COPYFILE_DISABLE=1 tar -czf "${FULL_BACKUP_TAR}" -C "$(dirname "${APP_DIR}")" "$(basename "${APP_DIR}")"
|
|
|
|
say "单独备份持久目录"
|
|
tmp_persist_dir="$(mktemp -d "${TMPDIR:-/tmp}/memind-105-persisted.XXXXXX")"
|
|
for item in "${PERSISTED_ITEMS[@]}"; do
|
|
if [[ -e "${APP_DIR}/${item}" ]]; then
|
|
mkdir -p "${tmp_persist_dir}/$(dirname "${item}")"
|
|
cp -a "${APP_DIR}/${item}" "${tmp_persist_dir}/$(dirname "${item}")/"
|
|
fi
|
|
done
|
|
COPYFILE_DISABLE=1 tar -czf "${PERSIST_BACKUP_TAR}" -C "${tmp_persist_dir}" .
|
|
rm -rf "${tmp_persist_dir}"
|
|
|
|
say "停止旧 Portal 服务"
|
|
systemctl stop "${SERVICE_NAME}"
|
|
lsof -tiTCP:8080 -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
|
sleep 2
|
|
|
|
say "准备新的 runtime live 目录"
|
|
rm -rf "${RUNTIME_DIR}"
|
|
mkdir -p "${RUNTIME_DIR}"
|
|
tar -xzf "${BUNDLE}" -C "${RUNTIME_DIR}"
|
|
cp "${MANIFEST}" "${RUNTIME_DIR}/.release-manifest.txt"
|
|
|
|
for item in "${PERSISTED_ITEMS[@]}"; do
|
|
if [[ -e "${APP_DIR}/${item}" ]]; then
|
|
rm -rf "${RUNTIME_DIR:?}/${item}"
|
|
mkdir -p "$(dirname "${RUNTIME_DIR}/${item}")"
|
|
cp -a "${APP_DIR}/${item}" "$(dirname "${RUNTIME_DIR}/${item}")/"
|
|
fi
|
|
done
|
|
|
|
say "在 105 安装 Linux 运行依赖"
|
|
cd "${RUNTIME_DIR}"
|
|
rm -rf node_modules package-lock.json
|
|
npm install --omit=dev --no-package-lock
|
|
|
|
say "归档旧 live 并切换新 runtime"
|
|
rm -rf "${OLD_LIVE_DIR}"
|
|
mv "${APP_DIR}" "${OLD_LIVE_DIR}"
|
|
mv "${RUNTIME_DIR}" "${APP_DIR}"
|
|
|
|
say "启动新的 Portal runtime"
|
|
systemctl restart "${SERVICE_NAME}"
|
|
|
|
say "健康检查"
|
|
for _ in $(seq 1 60); do
|
|
portal_code="$(curl -s -o /dev/null -w '%{http_code}' "${HEALTH_URL}" || true)"
|
|
if [[ "${portal_code}" == "200" ]]; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
portal_code="$(curl -s -o /dev/null -w '%{http_code}' "${HEALTH_URL}" || true)"
|
|
if [[ "${portal_code}" != "200" ]]; then
|
|
echo "health check failed: portal=${portal_code}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
say "runtime 切换完成"
|
|
REMOTE_SCRIPT
|
|
|
|
say "105 runtime 发布完成"
|