Author SHA1 Message Date
Hermes CI Fix 7aebe96bb3 feat(deploy): T2 frontend-layer assertions after [3] (insert-only)
3 independent hard gates: (1) per-file md5 reconciliation dist/assets vs webroot/assets, (2) entry chunk from webroot/index.html must exist in dist with same md5, (3) every chunk referenced by the main bundle must return HTTP 200 from SITE_URL. Env overrides FRONTEND_DIST_DIR / CMA_WEBROOT / SITE_URL for offline stub rehearsal (no build). No push to main.
2026-09-18 02:13:19 +08:00
+97
View File
@@ -48,6 +48,103 @@ rm -rf /var/www/cma/assets/
cp -r dist/* /var/www/cma/
echo " ✓ 部署完成"
# 3b. 前端层一致性断言(T2, 2026-09-18):部署链静默替换门
# 背景:[3] 只有 rm -rf + cp,缓存层吐旧 index、或 index 指向已被删的旧 chunk 时,现象是
# 「首页 200、点进页面 404/白屏」,而其后的冒烟 5 项全为后端项(127.0.0.1:8010)→ 整轮部署全绿。
# 三条断言各自独立输出,任一不一致即 exit 1。离线演练只覆盖下面三个变量,无需 build。
# >>> cma-frontend-assert(最小可抽取块:scripts/tests 按行首标记 awk 抽取后原样执行,改结构请沿用标记)
FRONTEND_DIST_DIR="${FRONTEND_DIST_DIR:-$SCRIPT_DIR/frontend/dist}"
CMA_WEBROOT="${CMA_WEBROOT:-/var/www/cma}"
SITE_URL="${SITE_URL:-https://cma.sxbh.ltd}"
echo "[3b/5] 前端层一致性断言(dist ↔ webroot ↔ 引用链)..."
FRONTEND_ASSERT_FAIL=0
# ① 逐文件 md5 对账:dist/assets 与 webroot/assets 文件集合一致(无多、无少)且同名文件 md5 相同
DIST_ASSETS="$FRONTEND_DIST_DIR/assets"
WEB_ASSETS="$CMA_WEBROOT/assets"
DIST_FILES="$(ls -1 "$DIST_ASSETS" 2>/dev/null | sort)"
WEB_FILES="$(ls -1 "$WEB_ASSETS" 2>/dev/null | sort)"
MISSING_FILES="$(comm -23 <(printf '%s\n' "$DIST_FILES") <(printf '%s\n' "$WEB_FILES"))"
EXTRA_FILES="$(comm -13 <(printf '%s\n' "$DIST_FILES") <(printf '%s\n' "$WEB_FILES"))"
MD5_DIFF=""
while IFS= read -r F; do
[ -n "$F" ] || continue
D_MD5="$(md5sum "$DIST_ASSETS/$F" 2>/dev/null | awk '{print $1}')"
W_MD5="$(md5sum "$WEB_ASSETS/$F" 2>/dev/null | awk '{print $1}')"
[ "$D_MD5" = "$W_MD5" ] || MD5_DIFF="${MD5_DIFF}${MD5_DIFF:+, }$F(dist=$D_MD5 web=$W_MD5)"
done <<< "$DIST_FILES"
if [ -n "$MISSING_FILES" ] || [ -n "$EXTRA_FILES" ] || [ -n "$MD5_DIFF" ]; then
echo " ❌ 断言① 逐文件 md5 对账失败(dist=$DIST_ASSETS)"
[ -z "$MISSING_FILES" ] || echo " webroot 缺失: $(printf '%s' "$MISSING_FILES" | tr '\n' ' ')"
[ -z "$EXTRA_FILES" ] || echo " webroot 多出: $(printf '%s' "$EXTRA_FILES" | tr '\n' ' ')"
[ -z "$MD5_DIFF" ] || echo " md5 不一致: $MD5_DIFF"
FRONTEND_ASSERT_FAIL=1
else
echo " ✅ 断言① 逐文件 md5 对账通过($(printf '%s\n' "$DIST_FILES" | awk 'NF{n++} END{print n+0}') 个文件全部一致)"
fi
# ② 主包一致:webroot/index.html 解析出的入口 chunk 必须在 dist 存在且 md5 相同
INDEX_HTML="$CMA_WEBROOT/index.html"
MAIN_BUNDLE="$(grep -o '/assets/index-[A-Za-z0-9._-]*\.js' "$INDEX_HTML" 2>/dev/null | head -n 1)"
if [ -z "$MAIN_BUNDLE" ]; then
echo " ❌ 断言② 主包一致失败: 未能从 $INDEX_HTML 解析出入口 chunk(/assets/index-*.js)"
FRONTEND_ASSERT_FAIL=1
elif [ ! -f "$FRONTEND_DIST_DIR$MAIN_BUNDLE" ]; then
echo " ❌ 断言② 主包一致失败: index.html 引用的 $MAIN_BUNDLE 在 dist 不存在($FRONTEND_DIST_DIR)"
FRONTEND_ASSERT_FAIL=1
else
MAIN_D_MD5="$(md5sum "$FRONTEND_DIST_DIR$MAIN_BUNDLE" 2>/dev/null | awk '{print $1}')"
MAIN_W_MD5="$(md5sum "$CMA_WEBROOT$MAIN_BUNDLE" 2>/dev/null | awk '{print $1}')"
if [ -n "$MAIN_D_MD5" ] && [ "$MAIN_D_MD5" = "$MAIN_W_MD5" ]; then
echo " ✅ 断言② 主包一致通过($MAIN_BUNDLE dist=web=$MAIN_D_MD5)"
else
echo " ❌ 断言② 主包一致失败: $MAIN_BUNDLE dist=$MAIN_D_MD5 web=$MAIN_W_MD5"
FRONTEND_ASSERT_FAIL=1
fi
fi
# ③ 引用链可 200:主包内引用的懒加载 chunk 逐个请求 $SITE_URL,必须全部 HTTP 200(无静默跳过分支)
# 字面量形态取并集:现网 Vite 产物实测为相对形态 from"./Login-CCqwmqG_.js",另有 "/assets/*.js" 形态;
# 两者都归一到 /assets/<name> 再请求。解析不到任何引用 → 显式判失败(不得静默放行)。
MAIN_JS_FILE="$CMA_WEBROOT$MAIN_BUNDLE"
CHUNKS="$(grep -o -E '(\./|/assets/)[A-Za-z0-9._-]+\.js' "$MAIN_JS_FILE" 2>/dev/null | sed -E 's#^\./#/assets/#' | sort -u)"
CHUNK_TOTAL="$(printf '%s\n' "$CHUNKS" | awk 'NF{n++} END{print n+0}')"
if [ ! -f "$MAIN_JS_FILE" ]; then
echo " ❌ 断言③ 引用链可 200 失败: 主包在 webroot 不存在($MAIN_JS_FILE)"
FRONTEND_ASSERT_FAIL=1
elif [ "$CHUNK_TOTAL" -eq 0 ]; then
echo " ❌ 断言③ 引用链可 200 失败: 主包 $MAIN_BUNDLE 内未解析到任何 chunk 引用(./*.js 或 /assets/*.js)"
FRONTEND_ASSERT_FAIL=1
else
CHUNK_OK=0
CHUNK_BAD=""
while IFS= read -r C; do
[ -n "$C" ] || continue
C_CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$SITE_URL$C" 2>/dev/null || echo 000)"
if [ "$C_CODE" = "200" ]; then
CHUNK_OK=$((CHUNK_OK + 1))
else
CHUNK_BAD="${CHUNK_BAD}${CHUNK_BAD:+, }$C(HTTP=$C_CODE)"
fi
done <<< "$CHUNKS"
if [ -n "$CHUNK_BAD" ]; then
echo " ❌ 断言③ 引用链可 200 失败($SITE_URL): $CHUNK_BAD"
FRONTEND_ASSERT_FAIL=1
else
echo " ✅ 断言③ 引用链可 200 通过($CHUNK_OK/$CHUNK_TOTAL chunk @ $SITE_URL)"
fi
fi
# 三条均为硬门:任一不一致即中止部署(nginx 直接服务静态文件,无需 reload)
if [ "$FRONTEND_ASSERT_FAIL" -ne 0 ]; then
echo ""
echo "🚨 前端层一致性断言未通过 → 中止部署(webroot 可能停留在旧包 / 半包)"
echo " dist=$FRONTEND_DIST_DIR webroot=$CMA_WEBROOT site=$SITE_URL"
exit 1
fi
echo " ✓ 前端层一致性断言通过(① 逐文件 md5 ② 主包 ③ 引用链 200)"
# <<< cma-frontend-assert
# 4. 更新后端依赖(失败不阻断 — 依赖已在系统Python中,避免PEP668中断部署)
echo "[4/5] 更新后端依赖..."
cd "$SCRIPT_DIR/backend"