chore: gate memind_adm gray release candidates
This commit is contained in:
@@ -1 +0,0 @@
|
||||
26747
|
||||
@@ -1 +0,0 @@
|
||||
25645
|
||||
@@ -9,3 +9,7 @@ memind-lib/
|
||||
.vite/
|
||||
*.log
|
||||
logs/
|
||||
.adm-api.pid
|
||||
.adm-dev.pid
|
||||
.adm-preview.pid
|
||||
release-candidates/
|
||||
|
||||
+27
-2
@@ -83,10 +83,35 @@ nginx 示例见 `scripts/gadm-nginx.conf.example`(`/ops/` → preview,`/auth
|
||||
## 唯一入口
|
||||
|
||||
```bash
|
||||
bash scripts/release-prod.sh --dry-run
|
||||
# 只生成持久候选包;不连接、不修改 103
|
||||
TEST_MEMIND_ROOT=<clean-Memind-worktree> \
|
||||
IMAGE_MAKE_RELEASE_ID=20260720-standalone-rc1 \
|
||||
bash scripts/release-prod.sh --package-only
|
||||
|
||||
# 候选包生成后,只读检查 103 灰度前置条件
|
||||
bash scripts/check-gray-ready.sh --candidate-dir release-candidates/<release-id>
|
||||
|
||||
# 正式生产发布;必须在灰度通过并得到明确许可后执行
|
||||
bash scripts/release-prod.sh
|
||||
```
|
||||
|
||||
`--dry-run` 是 `--package-only` 的兼容别名。候选包默认保存在
|
||||
`release-candidates/<release-id>/`,包含 tar 包、manifest 和 SHA-256 校验文件。
|
||||
|
||||
发包时 `Memind` 共享源码必须满足:
|
||||
|
||||
1. 是 Git 工作区且完全干净。
|
||||
2. `HEAD` 精确匹配 `MEMIND_BASE_REF`(默认 `origin/main`)。
|
||||
3. 包含 `asset-gateway.mjs`、`image-make-admin-config.mjs`、`image-make-client.mjs`。
|
||||
4. manifest 明确记录 `IMAGE_MAKE_RELEASE_ID`,避免后台候选与错误的 image_make 版本配套。
|
||||
|
||||
灰度分两层:
|
||||
|
||||
1. `check-gray-ready.sh` 默认只做后台候选包、103 当前健康状态、灰度端口和 image_make 版本的只读预检。
|
||||
2. `--require-image-integration` 额外要求 Portal runtime-config 路由、image_make 配置 URL 和 worker 均已就绪。该条件涉及生产运行态,未经单独许可不得开启。
|
||||
|
||||
`release-prod.sh` 没有“灰度即生产”的隐式行为:不带 `--package-only` 时就是正式 live 原子切换,必须等灰度验收通过后再执行。
|
||||
|
||||
## 产物发布流程
|
||||
|
||||
1. 本地生成 `memind-adm-<release-id>.tar.gz` 和发布清单。
|
||||
@@ -94,7 +119,7 @@ bash scripts/release-prod.sh
|
||||
3. 103 备份当前 `/Users/john/Project/memind_adm` 为时间戳压缩包。
|
||||
4. 在 `/Users/john/Project/releases/` 下解包新版本。
|
||||
5. 从当前 live 目录继承运行态配置与日志文件。
|
||||
6. 在 release 目录安装依赖、构建。
|
||||
6. 在 release 目录安装锁定依赖;前端 `dist/` 已在本地构建完成,103 不执行前端构建。
|
||||
7. 原子切换 `memind_adm` live 目录。
|
||||
8. 调用 `scripts/remote_restart.sh` 重启 `8085` 和 `5174`。
|
||||
9. 检查 `127.0.0.1:8085/health` 与 `127.0.0.1:5174/`。
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
.mindops
|
||||
node_modules
|
||||
coverage
|
||||
release-candidates
|
||||
*.log
|
||||
adm-preview.log
|
||||
adm-api.log
|
||||
@@ -33,6 +34,7 @@ scripts/local_restart.sh
|
||||
scripts/pro_restart.sh
|
||||
scripts/rsync_to_server.sh
|
||||
scripts/audit-103-state.sh
|
||||
scripts/check-gray-ready.sh
|
||||
scripts/gadm-nginx-105.conf
|
||||
scripts/gadm-nginx.conf.example
|
||||
scripts/gadm-lb.Caddyfile
|
||||
|
||||
Executable
+152
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HOST="${STUDIO_HOST:-10.10.0.2}"
|
||||
GRAY_WEB_PORT="${GRAY_WEB_PORT:-15174}"
|
||||
GRAY_API_PORT="${GRAY_API_PORT:-18085}"
|
||||
REQUIRE_IMAGE_INTEGRATION=0
|
||||
CANDIDATE_DIR=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash scripts/check-gray-ready.sh --candidate-dir DIR [--require-image-integration]
|
||||
|
||||
Read-only checks only. This script never uploads, restarts, or changes 103.
|
||||
|
||||
Checks:
|
||||
- candidate checksum and traceability manifest
|
||||
- current memind_adm production health
|
||||
- reserved gray ports are free on 103
|
||||
- expected image_make release is healthy on 103
|
||||
- optional end-to-end Portal/image_make runtime wiring readiness
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--candidate-dir)
|
||||
shift
|
||||
[[ $# -gt 0 ]] || { echo "--candidate-dir requires a directory" >&2; exit 2; }
|
||||
CANDIDATE_DIR="$1"
|
||||
;;
|
||||
--require-image-integration) REQUIRE_IMAGE_INTEGRATION=1 ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
[[ -n "${CANDIDATE_DIR}" ]] || { usage >&2; exit 2; }
|
||||
if [[ "${CANDIDATE_DIR}" != /* ]]; then
|
||||
CANDIDATE_DIR="${ROOT}/${CANDIDATE_DIR}"
|
||||
fi
|
||||
[[ -d "${CANDIDATE_DIR}" ]] || { echo "Candidate directory not found: ${CANDIDATE_DIR}" >&2; exit 1; }
|
||||
|
||||
manifest="$(find "${CANDIDATE_DIR}" -maxdepth 1 -type f -name '*.manifest.txt' -print -quit)"
|
||||
sha_file="$(find "${CANDIDATE_DIR}" -maxdepth 1 -type f -name '*.sha256' -print -quit)"
|
||||
[[ -n "${manifest}" && -n "${sha_file}" ]] || {
|
||||
echo "Candidate manifest or checksum file missing in ${CANDIDATE_DIR}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
(
|
||||
cd "${CANDIDATE_DIR}"
|
||||
shasum -a 256 -c "$(basename "${sha_file}")"
|
||||
)
|
||||
|
||||
manifest_value() {
|
||||
local key="$1"
|
||||
sed -n "s/^${key}=//p" "${manifest}" | head -n 1
|
||||
}
|
||||
|
||||
release_id="$(manifest_value release_id)"
|
||||
git_head="$(manifest_value git_head)"
|
||||
memind_head="$(manifest_value memind_git_head)"
|
||||
memind_status="$(manifest_value memind_git_status)"
|
||||
image_make_release_id="$(manifest_value image_make_release_id)"
|
||||
|
||||
[[ -n "${release_id}" && -n "${git_head}" && -n "${memind_head}" ]] || {
|
||||
echo "Candidate manifest is missing release traceability fields" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "${memind_status}" == "clean" ]] || {
|
||||
echo "Candidate manifest does not prove a clean Memind shared source" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "${image_make_release_id}" != "not-declared" && -n "${image_make_release_id}" ]] || {
|
||||
echo "Candidate manifest must declare IMAGE_MAKE_RELEASE_ID" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "candidate_release=${release_id}"
|
||||
echo "candidate_git_head=${git_head}"
|
||||
echo "candidate_memind_head=${memind_head}"
|
||||
echo "candidate_image_make_release=${image_make_release_id}"
|
||||
|
||||
remote_result="$(ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" \
|
||||
"GRAY_WEB_PORT='${GRAY_WEB_PORT}' GRAY_API_PORT='${GRAY_API_PORT}' IMAGE_MAKE_RELEASE_ID='${image_make_release_id}' REQUIRE_IMAGE_INTEGRATION='${REQUIRE_IMAGE_INTEGRATION}' /bin/bash" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
api_code="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8085/health || true)"
|
||||
web_code="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:5174/ops/ || true)"
|
||||
image_code="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:18083/health || true)"
|
||||
[[ "${api_code}" == "200" && "${web_code}" == "200" && "${image_code}" == "200" ]] || {
|
||||
echo "health_failed admin_api=${api_code} admin_web=${web_code} image_make=${image_code}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for port in "${GRAY_WEB_PORT}" "${GRAY_API_PORT}"; do
|
||||
if lsof -nP -iTCP:"${port}" -sTCP:LISTEN -t | grep -q .; then
|
||||
echo "gray_port_in_use=${port}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
export DOCKER_HOST=unix:///Users/john/.colima/default/docker.sock
|
||||
docker_bin=/usr/local/bin/docker
|
||||
container="$(${docker_bin} ps --format '{{.Names}} {{.Ports}}' | awk '/18083/ {print $1; exit}')"
|
||||
[[ -n "${container}" ]] || { echo "image_make_container_missing" >&2; exit 1; }
|
||||
image="$(${docker_bin} inspect --format '{{.Config.Image}}' "${container}")"
|
||||
health="$(${docker_bin} inspect --format '{{.State.Health.Status}}' "${container}")"
|
||||
[[ "${image}" == *"${IMAGE_MAKE_RELEASE_ID}"* && "${health}" == "healthy" ]] || {
|
||||
echo "image_make_release_mismatch image=${image} health=${health}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
worker_enabled="$(${docker_bin} inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "${container}" | sed -n 's/^WORKER_ENABLED=//p' | head -n 1)"
|
||||
config_url_present=0
|
||||
if ${docker_bin} inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "${container}" | grep -q '^IMAGE_MAKE_MEMIND_CONFIG_URL=.'; then
|
||||
config_url_present=1
|
||||
fi
|
||||
portal_route_present=0
|
||||
if grep -q '/internal/image-make/runtime-config' /Users/john/Project/Memind/server.mjs 2>/dev/null; then
|
||||
portal_route_present=1
|
||||
fi
|
||||
|
||||
echo "admin_api=200 admin_web=200 image_make=200"
|
||||
echo "gray_ports_free=${GRAY_WEB_PORT},${GRAY_API_PORT}"
|
||||
echo "image_make_image=${image}"
|
||||
echo "image_make_worker_enabled=${worker_enabled:-unset}"
|
||||
echo "image_make_config_url_present=${config_url_present}"
|
||||
echo "portal_runtime_route_present=${portal_route_present}"
|
||||
|
||||
if [[ "${REQUIRE_IMAGE_INTEGRATION}" == "1" ]]; then
|
||||
[[ "${worker_enabled}" == "true" && "${config_url_present}" == "1" && "${portal_route_present}" == "1" ]] || {
|
||||
echo "image_make_integration_not_ready" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
REMOTE
|
||||
)"
|
||||
|
||||
printf '%s\n' "${remote_result}"
|
||||
echo "gray_preflight=passed"
|
||||
+88
-17
@@ -22,7 +22,10 @@ MANIFEST_PATH="${TMP_DIR}/memind-adm-${RELEASE_ID}.manifest.txt"
|
||||
SHA_PATH="${TMP_DIR}/memind-adm-${RELEASE_ID}.sha256"
|
||||
MEMIND_SRC="${TEST_MEMIND_ROOT:-${ROOT}/../Memind}"
|
||||
MEMIND_LIB="${ROOT}/memind-lib"
|
||||
DRY_RUN=0
|
||||
MEMIND_BASE_REF="${MEMIND_BASE_REF:-origin/main}"
|
||||
IMAGE_MAKE_RELEASE_ID="${IMAGE_MAKE_RELEASE_ID:-not-declared}"
|
||||
PACKAGE_ONLY=0
|
||||
OUTPUT_DIR=""
|
||||
SKIP_BUILD=0
|
||||
AUTO_YES=0
|
||||
|
||||
@@ -34,15 +37,19 @@ trap cleanup EXIT
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
bash scripts/release-prod.sh [--dry-run] [--skip-build] [--yes]
|
||||
bash scripts/release-prod.sh [--package-only|--dry-run] [--output-dir DIR] [--skip-build] [--yes]
|
||||
|
||||
说明:
|
||||
1. 本地构建 dist 并打包运行产物(不含 src/ 等开发源码)
|
||||
2. 上传到 103
|
||||
3. 103 全量备份当前 memind_adm
|
||||
4. 解包、安装依赖(不在 103 构建前端)
|
||||
5. 原子切换 live 目录并重启验证
|
||||
6. 旧 live 目录移入 archives,103 不再保留可编辑源码树
|
||||
1. 校验 memind_adm 与 Memind 共享源码均来自干净、可追溯的 commit
|
||||
2. 本地构建 dist 并打包运行产物(不含 src/ 等开发源码)
|
||||
3. --package-only / --dry-run 只生成持久候选包,不连接或修改 103
|
||||
4. 正式模式上传到 103,完成全量备份、原子切换、重启和健康检查
|
||||
5. 旧 live 目录移入 archives,103 不再保留可编辑源码树
|
||||
|
||||
环境:
|
||||
TEST_MEMIND_ROOT 干净的 Memind 共享源码目录,默认 ../Memind
|
||||
MEMIND_BASE_REF Memind 共享源码必须精确匹配的 ref,默认 origin/main
|
||||
IMAGE_MAKE_RELEASE_ID 写入 manifest 的 image_make 依赖版本
|
||||
|
||||
禁止:
|
||||
- 直接 rsync 到 103
|
||||
@@ -53,7 +60,12 @@ EOF
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--dry-run|--package-only) PACKAGE_ONLY=1 ;;
|
||||
--output-dir)
|
||||
shift
|
||||
[[ $# -gt 0 ]] || { echo "--output-dir 缺少目录参数" >&2; exit 2; }
|
||||
OUTPUT_DIR="$1"
|
||||
;;
|
||||
--skip-build) SKIP_BUILD=1 ;;
|
||||
--yes|-y) AUTO_YES=1 ;;
|
||||
-h|--help)
|
||||
@@ -93,14 +105,52 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "${MEMIND_SRC}" rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
|
||||
echo "Memind 共享源码不是可追溯 Git 工作区: ${MEMIND_SRC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ -n "$(git -C "${MEMIND_SRC}" status --porcelain=v1 --untracked-files=all)" ]]; then
|
||||
echo "Memind 共享源码工作区不干净,禁止打包: ${MEMIND_SRC}" >&2
|
||||
git -C "${MEMIND_SRC}" status --short --untracked-files=all >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "${MEMIND_SRC}" rev-parse --verify --quiet "${MEMIND_BASE_REF}" >/dev/null || {
|
||||
echo "Memind 共享源码缺少基线 ref: ${MEMIND_BASE_REF}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
MEMIND_GIT_HEAD="$(git -C "${MEMIND_SRC}" rev-parse HEAD)"
|
||||
MEMIND_BASE_HEAD="$(git -C "${MEMIND_SRC}" rev-parse "${MEMIND_BASE_REF}")"
|
||||
if [[ "${MEMIND_GIT_HEAD}" != "${MEMIND_BASE_HEAD}" ]]; then
|
||||
echo "Memind 共享源码必须精确匹配 ${MEMIND_BASE_REF}" >&2
|
||||
echo " HEAD=${MEMIND_GIT_HEAD}" >&2
|
||||
echo " ${MEMIND_BASE_REF}=${MEMIND_BASE_HEAD}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for required_shared_file in \
|
||||
user-auth.mjs \
|
||||
asset-gateway.mjs \
|
||||
image-make-admin-config.mjs \
|
||||
image-make-client.mjs; do
|
||||
[[ -f "${MEMIND_SRC}/${required_shared_file}" ]] || {
|
||||
echo "Memind 共享源码缺少发布依赖: ${required_shared_file}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
[[ -f "${IGNORE_FILE}" ]] || {
|
||||
echo "缺少忽略文件: ${IGNORE_FILE}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
say "本地预检查"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo release-ssh-ok" >/dev/null
|
||||
ssh -o BatchMode=yes "${HOST}" "test -d '${APP_DIR}' && test -f '${APP_DIR}/package.json'" >/dev/null
|
||||
if [[ "${PACKAGE_ONLY}" -ne 1 ]]; then
|
||||
say "103 正式发布预检查"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo release-ssh-ok" >/dev/null
|
||||
ssh -o BatchMode=yes "${HOST}" "test -d '${APP_DIR}' && test -f '${APP_DIR}/package.json'" >/dev/null
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_BUILD}" -ne 1 ]]; then
|
||||
say "本地构建 dist(103 不再执行前端构建)"
|
||||
@@ -120,7 +170,6 @@ fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
MEMIND_GIT_HEAD="$(git -C "${MEMIND_SRC}" rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
MEMIND_GIT_BRANCH="$(git -C "${MEMIND_SRC}" branch --show-current 2>/dev/null || echo detached)"
|
||||
|
||||
say "打包 Memind 共享模块到 memind-lib"
|
||||
@@ -150,6 +199,14 @@ fi
|
||||
echo "memind-lib 缺少 user-auth.mjs" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -f "${MEMIND_LIB}/image-make-admin-config.mjs" ]] || {
|
||||
echo "memind-lib 缺少 image_make 管理配置模块" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -f "${MEMIND_LIB}/image-make-client.mjs" ]] || {
|
||||
echo "memind-lib 缺少 image_make 客户端模块" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -f "${MEMIND_LIB}/skills/excel-analyst/SKILL.md" ]] || {
|
||||
echo "memind-lib 缺少 Excel Analyst 技能" >&2
|
||||
exit 1
|
||||
@@ -163,7 +220,7 @@ grep -q 'tkmind-excel' "${MEMIND_LIB}/capabilities.mjs" || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "${AUTO_YES}" -ne 1 && "${DRY_RUN}" -ne 1 ]]; then
|
||||
if [[ "${AUTO_YES}" -ne 1 && "${PACKAGE_ONLY}" -ne 1 ]]; then
|
||||
say "发布确认"
|
||||
echo "目标主机: ${HOST}"
|
||||
echo "目标目录: ${APP_DIR}"
|
||||
@@ -185,6 +242,10 @@ say "生成发布清单"
|
||||
echo "git_branch=$(git -C "${ROOT}" branch --show-current 2>/dev/null || echo detached)"
|
||||
echo "memind_git_head=${MEMIND_GIT_HEAD}"
|
||||
echo "memind_git_branch=${MEMIND_GIT_BRANCH}"
|
||||
echo "memind_base_ref=${MEMIND_BASE_REF}"
|
||||
echo "memind_base_head=${MEMIND_BASE_HEAD}"
|
||||
echo "memind_git_status=clean"
|
||||
echo "image_make_release_id=${IMAGE_MAKE_RELEASE_ID}"
|
||||
echo "git_status_begin"
|
||||
git -C "${ROOT}" status --short --branch || true
|
||||
echo "git_status_end"
|
||||
@@ -199,9 +260,19 @@ tar \
|
||||
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}"
|
||||
if [[ "${PACKAGE_ONLY}" -eq 1 ]]; then
|
||||
if [[ -z "${OUTPUT_DIR}" ]]; then
|
||||
OUTPUT_DIR="${ROOT}/release-candidates/${RELEASE_ID}"
|
||||
elif [[ "${OUTPUT_DIR}" != /* ]]; then
|
||||
OUTPUT_DIR="${ROOT}/${OUTPUT_DIR}"
|
||||
fi
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
cp "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}" "${OUTPUT_DIR}/"
|
||||
say "候选包生成完成;未连接或修改 103"
|
||||
echo "candidate_dir=${OUTPUT_DIR}"
|
||||
ls -lh "${OUTPUT_DIR}/$(basename "${BUNDLE_PATH}")" \
|
||||
"${OUTPUT_DIR}/$(basename "${MANIFEST_PATH}")" \
|
||||
"${OUTPUT_DIR}/$(basename "${SHA_PATH}")"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user