fix(deploy): 部署源改用已提交 origin/main 树(独立检出),根治「开发区任何 modified 即中止」
ci/woodpecker/push/main Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
单 project-20260921-deploy-dirtyguard-rootfix(任总 2026-09-21 10:28「根治」)。 问题:开发区 /root/cma-management → /srv/hermes/shared/cma-management 既是编辑区又是部署源, deploy.sh [0] 对任何 tracked modified 即 exit 1,多 agent 并行下反复自锁: - 2026-09-18 frontend/package.json(corepack 生成物) - 2026-09-21 10:09 batch5 在途 6 文件(放行卡住) - 2026-09-21 11:07 构建又写出 frontend/components.d.ts 改动: - deploy.sh:部署内容 = 已提交的 origin/main(独立部署检出 /srv/hermes/shared/cma-deploy: fetch -> checkout --detach -f -> clean -fdx);[0] 降级为告警(不再 exit); 新增 1.1 部署树自检(部署树必须无 tracked 修改)、1.2 运行时资产单源接线(.env/venv/node_modules)、 1d 后端可导入自检、运行树幂等切换(unit 改写前备份 + 一行回滚,cma-runtime-unit-switch 可抽取块); 保留 [1b] 鉴权面审计闸门(B 类缺口即中止)与 构建->铺站->重启->冒烟 全流程; 新增 drill 模式(离线演练:www 必须在 /var/www 之外、systemctl/nginx 必须是 PATH 桩、迁移只 --check)。 - deploy/cma-deploy-dirty-guard.sh:宿主前置闸门降级为告警(仅仓库不可用时失败关闭),不再写开发区。 - deploy/gitea-cma-deploy-hook.sh:[0] 段文案/语义同步(gate-trail 留痕链原样保留)。 - deploy/install-host-deploy-hook.sh:宿主组件安装/升级(备份 + md5 校验 + --check)。 演练证据(26/26 PASS):脏区旧版 [0] exit=1 / 新版整链 exit=0;开发区在途改动逐字节保留; 构造 B 类缺口时 [1b] 中止且未铺站;真机 drill 零生产影响。 未 push(发布须任总放行)。
This commit is contained in:
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# CMA 部署前「部署源」前置检查(宿主侧,由 gitea-cma-deploy-hook.sh 调用)
|
||||
# ------------------------------------------------------------
|
||||
# 版本: cma-deploy-source-decouple-20260921(单 project-20260921-deploy-dirtyguard-rootfix)
|
||||
#
|
||||
# 旧行为(cma-deploy-dirty-guard-20260918):开发工作区出现**任何** tracked modified 即 exit 1。
|
||||
# 初衷是「防 deploy.sh 里的 `git pull` 覆盖未提交改动」。但开发区同时是部署源 ⇒ 多 agent 并行下
|
||||
# 必然反复自锁(corepack 生成物、在途任务文件、unplugin-vue-components 生成物…),发布链被自己的
|
||||
# 纪律闸门卡死(09-18 / 09-21 两次实测)。
|
||||
#
|
||||
# 新行为:部署内容 = **已提交的 origin/main 树**(deploy.sh 用独立检出 $CMA_DEPLOY_SRC 重建;
|
||||
# 部署过程不再对开发区做 pull/stash/checkout)。开发区的脏净与部署内容无关 ⇒ 本闸门**降级为告警**:
|
||||
# 打印开发区状态供留痕,不因 modified 中止部署。真正的硬闸门仍有三道,且都在 deploy.sh 里:
|
||||
# [0] 部署源解析(fetch 失败即中止) [1c] 部署树 ≠ 已提交树即中止 [1b] 鉴权面审计 B 类缺口即中止。
|
||||
# 本脚本只在「仓库不可用」这一种情况失败关闭(部署本身需要这个仓库)。
|
||||
#
|
||||
# 用法: cma-deploy-dirty-guard.sh [repo_dir] # 默认 /root/cma-management
|
||||
# 退出码: 0 = 可继续部署(无论开发区脏净); 1 = 仓库不可用
|
||||
# 依赖: git。本脚本**不写**开发区任何文件(旧版会把 corepack 生成物 checkout 回去,已移除)。
|
||||
# ============================================================
|
||||
set -uo pipefail
|
||||
|
||||
REPO="${1:-/root/cma-management}"
|
||||
|
||||
cd "$REPO" 2>/dev/null || { echo "❌ 仓库不可用: $REPO"; exit 1; }
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
echo "❌ 不是 git 仓库: $REPO"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')"
|
||||
HEAD_SHA="$(git rev-parse HEAD 2>/dev/null || echo '?')"
|
||||
ORIGIN_SHA="$(git rev-parse origin/main 2>/dev/null || echo '?')"
|
||||
DIRTY="$(git status --porcelain 2>/dev/null | grep -vE "__pycache__|^\?\?" | head -20)"
|
||||
DIRTY_N="$(git status --porcelain 2>/dev/null | grep -vcE "__pycache__|^\?\?")"
|
||||
|
||||
echo " ℹ️ 开发区 $REPO: 分支=$BRANCH HEAD=${HEAD_SHA:0:8} origin/main=${ORIGIN_SHA:0:8}"
|
||||
if [ "$DIRTY_N" -gt 0 ]; then
|
||||
echo " ⚠️ 开发区有 $DIRTY_N 处未提交 tracked 改动 —— 不阻断部署(这些改动不会上线,也不会被丢弃):"
|
||||
echo "$DIRTY" | sed 's/^/ /'
|
||||
else
|
||||
echo " ✓ 开发区无未提交 tracked 改动"
|
||||
fi
|
||||
echo " ℹ️ 部署内容 = 已提交的 origin/main 树(与开发区脏净解耦)"
|
||||
exit 0
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
# Gitea post-receive 部署触发器(由 /root/.ssh/authorized_keys 的 forced command 调用)
|
||||
# 落地:ops-20260918-gitea-deploy-hook-ssh-fix
|
||||
# 容器侧私钥只能触发本脚本(不能执行任意 root 命令),且来源 IP 限定在 gitea 容器网段。
|
||||
# HOME 必须显式为 /root:profile HOME 下 corepack 缓存缺 bin/pnpm.cjs → pnpm build 报 MODULE_NOT_FOUND。
|
||||
# 2026-09-18(0-P1-project-20260918-deploy-dirty-guard):deploy.sh 之前增加「提交纪律」前置闸门
|
||||
# —— frontend/package.json 的 corepack 生成物(packageManager 单字段)不再永久卡死发布链,
|
||||
# 其它任何脏改动仍照旧拦截(deploy.sh 第 0 步原闸门保留,双重保险)。
|
||||
# 2026-09-21(cma-deploy-source-decouple-20260921,单 project-20260921-deploy-dirtyguard-rootfix):
|
||||
# 根治「开发区任何 modified 即中止」——部署内容改为**已提交的 origin/main 树**(deploy.sh 用独立检出
|
||||
# 重建;部署过程不再 pull 开发区)⇒ 本前置闸门降级为**告警**:打印开发区状态供留痕,不因 modified 拦截;
|
||||
# 仅在「仓库不可用」时失败关闭(部署本身仍需这个仓库)。
|
||||
set -uo pipefail
|
||||
export HOME=/root
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export LANG=C.UTF-8
|
||||
|
||||
# ============================================================
|
||||
# [gate-trail] 动作前留痕(树外执行体入口,协议 v1.0.0)
|
||||
# chokepoint : gitea-cma-deploy-hook(channel=remote-trigger)
|
||||
# 注册表 : /etc/gate-trail/governed-entries.json
|
||||
# 留痕 : /var/log/gate-trail/trail.jsonl(append-only)
|
||||
# 覆盖语义 : 父级入口覆盖下游子步骤(deploy.sh → db_migrate.py --apply),
|
||||
# 单点挂载即覆盖整条链,下游不再各自留痕(见注册表 also_covered_by)
|
||||
# 依据 : 派单 yanxue-20260921-deploy-channel-gate-trail-mount
|
||||
# (协议母版 yanxue-20260918-051623-tree-external-unit-gate-trail)
|
||||
# ⚠️ 失败关闭 : begin 返回 2(HIGH/CRITICAL 且无有效审批)→ 中止部署
|
||||
# ⚠️ 取值纪律 : 必须先接住输出再取 $?;写成 `gate-trail ... | python3` 会把 exit 2 读成 0
|
||||
# ⚠️ 收尾纪律 : 用 EXIT trap 闭合(含 dirty-guard 拦截等提前 exit 路径)
|
||||
# ============================================================
|
||||
TRAIL_CHANNEL="remote-trigger"
|
||||
TRAIL_ACTION="Gitea push → post-receive → SSH forced-command → CMA 部署链(deploy.sh)"
|
||||
TRAIL_CMD="bash /root/cma-management/deploy.sh (trigger: gitea post-receive / authorized_keys forced-command)"
|
||||
TRAIL_TASK="${GATE_TRAIL_TASK:-}"
|
||||
TRAIL_ID=""
|
||||
TRAIL_CLOSED=""
|
||||
|
||||
# 收尾:任何退出路径都闭合留痕(BEGIN 之后才注册 trap,避免无 ID 时空转)
|
||||
trail_closeup() {
|
||||
local _rc=$?
|
||||
if [ -z "${TRAIL_CLOSED:-}" ] && [ -n "${TRAIL_ID:-}" ] && command -v gate-trail >/dev/null 2>&1; then
|
||||
if [ "$_rc" -eq 0 ]; then
|
||||
gate-trail end --id "$TRAIL_ID" --result pass \
|
||||
--evidence "deploy-hook exit=0 @ $(date '+%F %T %Z')" >/dev/null 2>&1 || true
|
||||
else
|
||||
gate-trail end --id "$TRAIL_ID" --result fail \
|
||||
--evidence "deploy-hook exit=$_rc @ $(date '+%F %T %Z')(含前置闸门拦截等中止路径)" >/dev/null 2>&1 || true
|
||||
fi
|
||||
TRAIL_CLOSED=1
|
||||
fi
|
||||
return "$_rc"
|
||||
}
|
||||
|
||||
if command -v gate-trail >/dev/null 2>&1; then
|
||||
TRAIL_JSON=$(gate-trail begin --channel "$TRAIL_CHANNEL" --action "$TRAIL_ACTION" \
|
||||
--cmd "$TRAIL_CMD" --task "$TRAIL_TASK" \
|
||||
--asset /root/cma-management --json 2>/dev/null)
|
||||
rc=$?
|
||||
if [ "$rc" -eq 2 ]; then
|
||||
echo "[deploy-hook] ⛔ gate-trail 拒绝执行(高危且无有效审批)—— 中止部署" >&2
|
||||
exit 2
|
||||
fi
|
||||
TRAIL_ID=$(printf '%s' "$TRAIL_JSON" | python3 -c 'import json,sys
|
||||
try:
|
||||
print(json.load(sys.stdin).get("trail_id",""))
|
||||
except Exception:
|
||||
print("")')
|
||||
if [ -z "$TRAIL_ID" ]; then
|
||||
echo "[deploy-hook] ⛔ gate-trail 未返回 trail_id —— 按失败关闭处理,中止部署" >&2
|
||||
exit 2
|
||||
fi
|
||||
echo "[deploy-hook] 🧾 gate-trail 留痕已开: $TRAIL_ID"
|
||||
trap trail_closeup EXIT
|
||||
else
|
||||
echo "[deploy-hook] ⚠️ gate-trail 不可用,本次动作将无留痕(违反 gate-trail 协议)" >&2
|
||||
fi
|
||||
|
||||
|
||||
echo "[deploy-hook] 宿主机已接受 SSH(key=gitea-deploy-hook@sxbh.ltd, forced-command)"
|
||||
echo "[deploy-hook] 请求命令(被忽略/仅记录)=${SSH_ORIGINAL_COMMAND:-<none>}"
|
||||
|
||||
cd /root/cma-management || { echo "[deploy-hook] ✗ /root/cma-management 不可用"; exit 1; }
|
||||
|
||||
# [0/5] 部署源前置检查(实现见 cma-deploy-dirty-guard.sh)
|
||||
# 2026-09-21 起:开发区脏净**不再**中止部署(部署内容 = 已提交的 origin/main 树,见 deploy.sh);
|
||||
# 本步只打印开发区状态供留痕,仅「仓库不可用」失败关闭。
|
||||
DIRTY_GUARD=/usr/local/sbin/cma-deploy-dirty-guard.sh
|
||||
echo "[deploy-hook] [0/5] 部署源前置检查(开发区脏净仅告警)..."
|
||||
if [ ! -r "$DIRTY_GUARD" ]; then
|
||||
echo "[deploy-hook] ✗ 前置检查脚本缺失或不可读: $DIRTY_GUARD(按失败关闭中止部署)"
|
||||
exit 1
|
||||
fi
|
||||
if ! bash "$DIRTY_GUARD" /root/cma-management; then
|
||||
echo "[deploy-hook] ✗ 仓库不可用 → 中止部署"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash deploy.sh 2>&1
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo "[deploy-hook] ✓ 部署成功"
|
||||
else
|
||||
echo "[deploy-hook] ✗ 部署失败 (exit: $rc)"
|
||||
fi
|
||||
exit "$rc"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 宿主部署链组件安装/升级(CMA)
|
||||
# cma-deploy-source-decouple-20260921(单 project-20260921-deploy-dirtyguard-rootfix)
|
||||
#
|
||||
# 把本仓 deploy/ 下的宿主侧组件安装到 /usr/local/sbin/(改前自动备份、改后校验 md5)。
|
||||
# 组件(宿主侧,容器内 gitea post-receive → SSH forced-command 进入本仓库时执行):
|
||||
# gitea-cma-deploy-hook.sh → /usr/local/sbin/gitea-cma-deploy-hook.sh
|
||||
# cma-deploy-dirty-guard.sh → /usr/local/sbin/cma-deploy-dirty-guard.sh
|
||||
#
|
||||
# 用法:
|
||||
# bash deploy/install-host-deploy-hook.sh # 安装/升级(含备份 + 校验)
|
||||
# bash deploy/install-host-deploy-hook.sh --check # 只比对(不写任何文件)
|
||||
#
|
||||
# 说明:
|
||||
# - 备份目录 /root/backups/cma-deploy-source-decouple-<YYYYMMDD-HHMMSS>/
|
||||
# - 一行回滚: cp <备份目录>/<文件名> /usr/local/sbin/<文件名> && md5sum -c <备份目录>/MD5SUMS.rollback
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SRC_DIR="$REPO_DIR/deploy"
|
||||
DST_DIR="/usr/local/sbin"
|
||||
FILES=(gitea-cma-deploy-hook.sh cma-deploy-dirty-guard.sh)
|
||||
MODE="${1:-install}"
|
||||
|
||||
for f in "${FILES[@]}"; do
|
||||
[ -r "$SRC_DIR/$f" ] || { echo "❌ 源文件缺失: $SRC_DIR/$f"; exit 1; }
|
||||
done
|
||||
|
||||
if [ "$MODE" = "--check" ]; then
|
||||
rc=0
|
||||
for f in "${FILES[@]}"; do
|
||||
if [ -r "$DST_DIR/$f" ] && cmp -s "$SRC_DIR/$f" "$DST_DIR/$f"; then
|
||||
echo "✓ 一致: $DST_DIR/$f"
|
||||
else
|
||||
echo "≠ 不一致: $DST_DIR/$f(需要安装)"; rc=1
|
||||
fi
|
||||
done
|
||||
exit "$rc"
|
||||
fi
|
||||
|
||||
TS="$(date +%Y%m%d-%H%M%S)"
|
||||
BAK_DIR="/root/backups/cma-deploy-source-decouple-$TS"
|
||||
mkdir -p "$BAK_DIR"
|
||||
|
||||
for f in "${FILES[@]}"; do
|
||||
if [ -e "$DST_DIR/$f" ]; then
|
||||
cp -a "$DST_DIR/$f" "$BAK_DIR/$f"
|
||||
md5sum "$DST_DIR/$f" >> "$BAK_DIR/MD5SUMS.rollback"
|
||||
else
|
||||
echo "<absent>" >> "$BAK_DIR/MD5SUMS.rollback"
|
||||
fi
|
||||
done
|
||||
|
||||
for f in "${FILES[@]}"; do
|
||||
install -m 0755 -o root -g root "$SRC_DIR/$f" "$DST_DIR/$f"
|
||||
done
|
||||
|
||||
echo "备份目录: $BAK_DIR"
|
||||
for f in "${FILES[@]}"; do
|
||||
echo "已安装: $DST_DIR/$f md5=$(md5sum "$DST_DIR/$f" | awk '{print $1}')"
|
||||
done
|
||||
bash -n "$DST_DIR/gitea-cma-deploy-hook.sh" && echo "✓ 语法检查通过"
|
||||
echo "✓ 安装完成(生效于下一次 push 触发的部署)"
|
||||
Reference in New Issue
Block a user