#!/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:-}" 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"