Merge origin/main into feature/svc-d1-identity-flow
ci / test-pg (pull_request) Successful in 1m52s
ci / test (pull_request) Failing after 2m51s

Why: main already has one release transaction, so this branch keeps that path and only isolates update and recover in a clean child.
Evidence: SELF bash -n scripts/svc.sh; python3 -m unittest identity/recover/deploy/u2/lifecycle pin 92 tests OK (2 skipped), then 66 tests OK after dropping the obsolete journal hook.
Impact: deploy uses releases/ and deploy.journal; the lifecycle child reconciles that journal before deploy or recover. Backend pin is 0f192a4.
Refs: #485
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-12 15:06:55 +00:00
co-authored by Cursor
7 changed files with 1100 additions and 91 deletions
+358 -78
View File
@@ -1460,7 +1460,17 @@ env_config() {
ENV_DIR="$RUNTIME/$name"
ENV_PID="$ENV_DIR/svc.pid"
ENV_LOG="$ENV_DIR/svc.log"
ENV_BIN_DIR="$ENV_DIR/bin"
# U2:releases/ 不可变版本目录 + current 原子指针。已切换实例经 current 解析
# 产物(进程 cmdline 保留未解析路径——指针切换不破坏 pidcheck 匹配,也彻底
# 消除 exe=(deleted) 覆盖孤儿);legacy 实例沿用原路径(首次 deploy 领养,
# svc_release_adopt_legacy)。
if [ -L "$ENV_DIR/current" ]; then
ENV_BIN_DIR="$ENV_DIR/current/bin"
ENV_PLUGINS="$ENV_DIR/current/plugins"
else
ENV_BIN_DIR="$ENV_DIR/bin"
ENV_PLUGINS="$ENV_DIR/plugins"
fi
ENV_BIN="$ENV_BIN_DIR/agent-system"
# 按实例重置内存态——批量 update/restart 逐服务解析时不串用上一服务。
# DB_*:防跨服务串库;SVC_PIDCHECK/EXEC/VERSION/SOURCE:防 pid_alive 与
@@ -1944,6 +1954,256 @@ install_artifacts() { # SRCBIN SRCPLUGINS:写入不可变版本目录并原子
return 0
}
# ---- U2:不可变版本目录(releases/)+ current 原子指针 + 切换 journal + 失败回退 ----
# 布局:releases/<id>/{bin/agent-system, plugins/, htdocs/, release.json};
# current → releases/<id>(相对符号链接,temp+rename 原子切换);deploy.journal
# 记录切换过程(staging→switched→done/rolledback/blocked),任意步骤中断后由
# svc_deploy_journal_resolve 判定并恢复实际状态。legacy 目录永不删除:旧进程
# cmdline 指向 legacy 路径,pidcheck 兼容到自然换版。
svc_release_cur() { [ -L "$ENV_DIR/current" ] && readlink "$ENV_DIR/current" || true; }
svc_release_cur_id() { local c; c="$(svc_release_cur)"; printf '%s' "${c##*/}"; }
# svc_release_adopt_legacy:legacy 布局(bin/ 直接落服务目录)→ 领养为首个
# release(幂等:已有 current 即跳过;无产物即跳过——全新部署无需领养)。
svc_release_adopt_legacy() {
[ -L "$ENV_DIR/current" ] && return 0
[ -x "$ENV_DIR/bin/agent-system" ] || return 0
# 注意:bash 5.3 set -u 下同一 local 行不得引用本行先声明变量(拆行)。
local id="legacy-$(date -u +%Y%m%dT%H%M%SZ)"
local dir="$ENV_DIR/releases/$id"
mkdir -p "$dir/bin" || return 1
cp -f "$ENV_DIR/bin/agent-system" "$dir/bin/agent-system" || return 1
if [ -d "$ENV_DIR/plugins" ]; then
cp -r "$ENV_DIR/plugins" "$dir/plugins" || return 1
else
mkdir -p "$dir/plugins"
fi
[ -d "$ENV_DIR/htdocs" ] && cp -r "$ENV_DIR/htdocs" "$dir/htdocs" || true
printf '{"id":"%s","source":"legacy","created":"%s"}\n' "$id" "$(date -u +%FT%TZ)" > "$dir/release.json"
ln -s "releases/$id" "$ENV_DIR/.current.new" && mv -Tf "$ENV_DIR/.current.new" "$ENV_DIR/current" || {
rm -f "$ENV_DIR/.current.new"; return 1; }
c_info "[$ENV_NAME] 领养 legacy 制品为首个 release:releases/$id"
}
# svc_release_id:源标签净化的版本目录名(rdm-ddc09eb-20261012T131500)。
svc_release_id() {
local label
label="$(printf '%s' "${SRC_LABEL:-manual}" | tr '@' '-' | tr -cd 'a-zA-Z0-9._-' | cut -c1-40)"
printf '%s-%s' "$label" "$(date -u +%Y%m%dT%H%M%SZ)"
}
# svc_release_stage ID SRCBIN SRCPLUGINS [HTDOCS]:候选产物入不可变版本目录。
# stdout 回显 release 目录(供门禁/探测引用)。
svc_release_stage() {
local id="$1" srcbin="$2" srcplug="$3" htdocs="${4:-}"
local dir="$ENV_DIR/releases/$id"
rm -rf "$dir"
mkdir -p "$dir/bin" || return 1
cp -f "$srcbin" "$dir/bin/agent-system" || return 1
if [ -d "$srcplug" ]; then
cp -r "$srcplug" "$dir/plugins" || return 1
else
mkdir -p "$dir/plugins"
fi
if [ -n "$htdocs" ] && [ -d "$htdocs" ]; then
cp -r "$htdocs" "$dir/htdocs" || return 1
fi
printf '{"id":"%s","source":"%s","created":"%s"}\n' \
"$id" "${SRC_LABEL:-}" "$(date -u +%FT%TZ)" > "$dir/release.json"
printf '%s' "$dir"
}
# svc_release_switch ID:原子切换 current(temp 符号链接 + rename)。
svc_release_switch() {
ln -s "releases/$1" "$ENV_DIR/.current.new" || return 1
mv -Tf "$ENV_DIR/.current.new" "$ENV_DIR/current" || {
rm -f "$ENV_DIR/.current.new"; return 1; }
}
# svc_release_gc [KEEP]:裁剪历史 release(保留最近 KEEP 个,缺省 5;current 与
# legacy 永不删除)。失败静默——目录非关键路径。
svc_release_gc() {
local keep="${1:-5}" cur id dirs=()
[ -d "$ENV_DIR/releases" ] || return 0
cur="$(svc_release_cur_id)"
while IFS= read -r d; do dirs+=("$(basename "$d")"); done \
< <(ls -1dt "$ENV_DIR/releases"/*/ 2>/dev/null)
local n=${#dirs[@]} i rmv
for ((i = keep; i < n; i++)); do
rmv="${dirs[$i]}"
[ "$rmv" = "$cur" ] && continue
case "$rmv" in legacy-*) continue ;; esac
rm -rf "$ENV_DIR/releases/$rmv"
done
return 0
}
# ---- 切换 journal(deploy.journal):python3 原子写;缺 python3 时降级为无 journal ----
svc_journal_path() { printf '%s' "$ENV_DIR/deploy.journal"; }
svc_journal_write() { # PHASE [k=v ...]
local phase="$1"; shift
command -v python3 >/dev/null 2>&1 || return 0
python3 - "$phase" "$(svc_journal_path)" "$@" <<'PYJ'
import json, os, sys, time
phase, out, args = sys.argv[1], sys.argv[2], sys.argv[3:]
j = {"phase": phase, "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
for kv in args:
if "=" in kv:
k, v = kv.split("=", 1)
j[k] = v
tmp = out + ".new"
with open(tmp, "w") as f:
f.write(json.dumps(j, ensure_ascii=False) + "\n")
os.replace(tmp, out)
PYJ
}
svc_journal_get() { # KEY → 值(journal 缺失/损坏 → 空)
local f; f="$(svc_journal_path)"
[ -f "$f" ] || return 0
python3 -c 'import json,sys
try:
d = json.load(open(sys.argv[1])); v = d.get(sys.argv[2], "")
print(v if isinstance(v, str) else json.dumps(v))
except Exception:
pass' "$f" "$1" 2>/dev/null
}
svc_journal_phase() { svc_journal_get phase; }
svc_journal_clear() { rm -f "$(svc_journal_path)"; }
# svc_db_schema_version:SQLite 档当前 schema(schema_migrations 最大 id;
# 无库/无表/pg → 空)。只读连接,绝不写库。
svc_db_schema_version() {
local db="$ENV_DATA/agent-system.db"
[ -f "$db" ] || return 0
python3 -c 'import sqlite3,sys
try:
con = sqlite3.connect("file:" + sys.argv[1] + "?mode=ro", uri=True)
print(con.execute("select max(id) from schema_migrations").fetchone()[0])
con.close()
except Exception:
pass' "$db" 2>/dev/null
}
# svc_release_schema_supported ID:release.json 的 schema_at_commit(无则空)。
svc_release_schema_supported() {
local f="$ENV_DIR/releases/$1/release.json"
[ -f "$f" ] || return 0
python3 -c 'import json,sys
try:
print(json.load(open(sys.argv[1])).get("schema_at_commit") or "")
except Exception:
pass' "$f" 2>/dev/null
}
# svc_release_commit_schema ID:成功提交时把当前库 schema 记入 release.json
# (回退时据此判定旧制品可否运行;pg/无库静默跳过)。
svc_release_commit_schema() {
local f="$ENV_DIR/releases/$1/release.json" v
v="$(svc_db_schema_version)"
[ -n "$v" ] || return 0
python3 -c 'import json,sys
try:
d = json.load(open(sys.argv[1])); d["schema_at_commit"] = sys.argv[2]
open(sys.argv[1], "w").write(json.dumps(d, ensure_ascii=False) + "\n")
except Exception:
pass' "$f" "$v" 2>/dev/null || true
}
# svc_rollback_deploy REASON:切换失败 → 回退上一完整版本并恢复期望运行态。
# schema 阻断(库 schema 超过上一 release 提交时点)不自动回退启动、绝不触碰
# 业务数据库——登记 blocked 待受控迁移(U2 约束:不能靠覆盖数据库「回滚」)。
svc_rollback_deploy() {
local reason="$1"
local prev new desired pid cur_schema prev_schema
prev="$(svc_journal_get prev)"
new="$(svc_journal_get new)"
desired="$(svc_journal_get desired)"
pid="$(read_pid)"
if [ -n "$pid" ] && pid_alive "$pid"; then
c_warn "[$ENV_NAME] 回退前停止新制品进程 pid=$pid"
proc_stop "$pid" || true
rm -f "$ENV_PID"
fi
if [ -z "$prev" ] || [ ! -d "$ENV_DIR/releases/$prev" ]; then
c_err "[$ENV_NAME] 无可回退版本(prev 缺失,$reason)——保持停止;清理:rm 前先取证"
svc_journal_write blocked reason="无 prev release:$reason"
hist_event deploy fail "$pid" "回退无 prev($reason)"
return 1
fi
cur_schema="$(svc_db_schema_version)"
prev_schema="$(svc_release_schema_supported "$prev")"
if [ -n "$cur_schema" ] && [ -n "$prev_schema" ] \
&& [ "$cur_schema" -gt "$prev_schema" ] 2>/dev/null; then
c_err "[$ENV_NAME] schema 已前进(库 v$cur_schema > 上一 release 提交 v$prev_schema):旧制品无法运行,禁止自动回退"
c_err " 不触碰业务数据库。处理:deploy 换可运行制品(受控迁移后),或人工处置;登记 blocked"
svc_journal_write blocked reason="schema v$cur_schema > prev v$prev_schema($reason)"
hist_event deploy fail "$pid" "回退阻断:schema 前进($reason)"
return 1
fi
svc_release_switch "$prev" || {
svc_journal_write blocked reason="指针回退失败:$reason"
return 1
}
c_ok "[$ENV_NAME] 已回退上一版本:releases/$prev($reason)"
# 指针已切回:重解析产物路径到回退后的 release。
ENV_BIN_DIR="$ENV_DIR/current/bin"
ENV_PLUGINS="$ENV_DIR/current/plugins"
ENV_BIN="$ENV_BIN_DIR/agent-system"
hist_event deploy rollback "$pid" "new=${new:-?} prev=$prev($reason)"
svc_journal_write rolledback "prev=$prev" "new=${new:-}" "reason=$reason"
if [ "$desired" = "running" ]; then
c_info "[$ENV_NAME] 按原期望态恢复运行(上一版本)"
# 登记一致性:清掉新部署的源标签——回退后的 record_state 须回落到 svc.env
# 里上一版的 source(否则实际跑旧制品、登记却写新标签)。
SRC_LABEL=""
SVC_SOURCE="$(svc_get source)"
agent_do_start || { svc_journal_clear; return 1; }
fi
svc_journal_clear
return 0
}
# svc_deploy_journal_resolve [ACTION]:生命周期入口处理中断的切换。
# switched/verifying:指针已切但未经完整验证 → 回退上一完整版本(无 prev 的
# 首次部署中断 → 保持停止)。blocked:拒绝除 deploy 外的动作(显式换制品即
# 解除路径)。staging:未切换,清记录(孤儿 release 目录无害,随 gc 裁剪)。
svc_deploy_journal_resolve() {
local action="${1:-}" phase
[ -f "$(svc_journal_path)" ] || return 0
phase="$(svc_journal_phase)"
case "$phase" in
""|done|rolledback)
svc_journal_clear; return 0 ;;
staging)
c_warn "[$ENV_NAME] 发现中断的暂存(staging,未切换)——忽略"
svc_journal_clear; return 0 ;;
blocked)
if [ "$action" = "deploy" ]; then
c_warn "[$ENV_NAME] 存在 blocked 切换记录,本次 deploy 尝试跨越(换可运行制品)"
return 0
fi
c_err "[$ENV_NAME] 切换被阻断($(svc_journal_get reason)):先 deploy 换可运行制品,或受控迁移后再操作"
return 1 ;;
switched|verifying)
c_warn "[$ENV_NAME] 发现中断的切换($phase,new=$(svc_journal_get new))——回退上一完整版本"
svc_rollback_deploy "中断的切换(journal=$phase)" || return 1
return 0 ;;
esac
return 0
}
# svc_deploy_recycle_wt:deploy 各失败/成功出口统一回收构建 worktree(尽力而为)。
svc_deploy_recycle_wt() {
case "${SRC_KINDSRC:-}" in
dev) [ -n "${wt:-}" ] && recycle_worktree "$SRC_SHA" ;;
rdm) [ -n "${wt:-}" ] && recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR" ;;
esac
return 0
}
# materialize_dev_commit FULL SHA TARGET:隔离 worktree 物化+构建;stdout 只回显 worktree
# 路径(提示信息走 stderr)。TARGET=build(agent)| frontend(static)。
materialize_dev_commit() {
@@ -2213,40 +2473,26 @@ agent_do_deploy() {
return 1
fi
hist_event deploy begin "$(read_pid)" "from=$SRC_LABEL"
svc_reconcile_journal || return 1
SVC_WAS_RUNNING=0
pid_alive "$(read_pid)" && SVC_WAS_RUNNING=1
SVC_FROM_DESIRED="running"
svc_journal_write prepared || return 1
# U1/U2(构建前置):先构建/物化候选并过身份门禁,再停旧服务——构建失败或候选
# 不合格时旧服务继续运行、旧制品与登记不动(2026-10-12 rdm1/rdmcand 事故:停服
# →覆盖→启动即拒,只留「登记版本≠磁盘制品」的混合状态且无恢复路径)。
local wt RDM_UI_OK=0
# U2(不可变版本目录 + 失败回退):构建 → 暂存 release → 门禁(身份/环境 DSN)
# → 停旧 → current 原子切换 → 启动与 UI 验证 → 成功提交(schema 记录 + journal
# 清理 + release 裁剪);任一步失败回退上一完整版本并恢复期望运行态
# (svc_rollback_deploy;库 schema 已前进时阻断,绝不覆盖业务数据库)。
local wt relid staged srcbin srcplug srchtdocs="" prev_id desired
svc_release_adopt_legacy \
|| { hist_event deploy fail "$(read_pid)" "legacy 领养失败($SRC_LABEL)"; return 1; }
case "$SRC_KINDSRC" in
local)
c_info "[$ENV_NAME] 构建当前工作区(make build)——唯一写主检出构建产物的路径"
make -C "$ROOT" build || { hist_event deploy fail "$(read_pid)" "构建失败($SRC_LABEL)"; return 1; }
svc_deploy_identity_gate "$BIN" \
|| { hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"; return 1; }
svc_journal_write stopping || return 1
do_stop_quiet --keep-desired
install_artifacts "$BIN" "$ROOT/plugins" \
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
;;
make -C "$ROOT" build || {
c_err "[$ENV_NAME] 构建失败($SRC_LABEL)——旧服务保持运行、制品与登记不变"
hist_event deploy fail "$(read_pid)" "构建失败($SRC_LABEL)"
return 1
}
srcbin="$BIN"; srcplug="$ROOT/plugins" ;;
dev)
wt="$(materialize_dev_commit "$SRC_FULL" "$SRC_SHA" build)" \
|| { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; }
if ! svc_deploy_identity_gate "$wt/bin/agent-system"; then
hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"
recycle_worktree "$SRC_SHA"
return 1
fi
svc_journal_write stopping || return 1
do_stop_quiet --keep-desired
install_artifacts "$wt/bin/agent-system" "$wt/plugins" \
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
recycle_worktree "$SRC_SHA"
;;
srcbin="$wt/bin/agent-system"; srcplug="$wt/plugins" ;;
release)
local relbin="$REL_ROOT/build/$REL_VERSION/agent-system"
if [ ! -f "$relbin" ]; then
@@ -2255,75 +2501,98 @@ agent_do_deploy() {
hist_event deploy fail "$(read_pid)" "制品缺失($SRC_LABEL)"
return 1
fi
svc_deploy_identity_gate "$relbin" \
|| { hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"; return 1; }
svc_journal_write stopping || return 1
do_stop_quiet --keep-desired
materialize_release || { hist_event deploy fail "$(read_pid)" "物化/校验失败($SRC_LABEL)"; return 1; }
;;
local mf="$REL_ROOT/versions/$REL_VERSION/manifest.json" want have
if [ -f "$mf" ]; then
want="$(sed -n 's/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/\1/p' "$mf" | head -1)"
have="$(sha256sum "$relbin" | cut -d' ' -f1)"
if [ -n "$want" ] && [ "$want" != "$have" ]; then
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($relbin)——拒绝部署"
hist_event deploy fail "$(read_pid)" "sha256 校验失败($SRC_LABEL)"
return 1
fi
[ -n "$want" ] && c_ok "[$ENV_NAME] sha256 校验通过(manifest:$REL_VERSION)"
else
c_warn "[$ENV_NAME] 无 manifest 可校验:$mf"
fi
srcbin="$relbin"; srcplug="$REL_ROOT/build/$REL_VERSION/plugins" ;;
rdm)
wt="$(materialize_rdm_commit "$SRC_FULL" "$SRC_SHA")" \
|| { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; }
# RDM 组合插件编译进二进制(provides 无声明式资产)——插件目录按空集安装。
# RDM 组合插件编译进二进制(provides 无声明式资产)——插件目录按空集。
mkdir -p "$wt/.svc-empty-plugins"
if ! svc_deploy_identity_gate "$wt/bin/agent-system-rdm"; then
hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"
recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR"
return 1
fi
svc_journal_write stopping || return 1
do_stop_quiet --keep-desired
install_artifacts "$wt/bin/agent-system-rdm" "$wt/.svc-empty-plugins" \
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
# RDM 自带零构建工作台(web/)落 htdocs/;收尾自动部署伴生 ${name}web。
if [ -d "$wt/web" ]; then
rm -rf "$ENV_DIR/htdocs"
if cp -r "$wt/web" "$ENV_DIR/htdocs"; then
RDM_UI_OK=1
c_ok "[$ENV_NAME] RDM 工作台就位:$ENV_DIR/htdocs(将自动部署伴生 $(rdm_companion_name "$ENV_NAME"))"
else
c_warn "[$ENV_NAME] RDM web/ 工作台复制失败"
fi
else
c_warn "[$ENV_NAME] RDM 源无 web/ 工作台目录"
fi
recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR"
;;
srcbin="$wt/bin/agent-system-rdm"; srcplug="$wt/.svc-empty-plugins"
[ -d "$wt/web" ] && srchtdocs="$wt/web" ;;
esac
# 新产物承接能力复核(环境 DSN 交付):不支持即失败——绑定保持可恢复,不降级启动。
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$ENV_BIN"; then
relid="$(svc_release_id)"
staged="$(svc_release_stage "$relid" "$srcbin" "$srcplug" ${srchtdocs:+"$srchtdocs"})" \
|| { hist_event deploy fail "$(read_pid)" "产物暂存失败($SRC_LABEL)"; svc_deploy_recycle_wt; return 1; }
if ! svc_deploy_identity_gate "$staged/bin/agent-system"; then
hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"
rm -rf "$staged"
svc_deploy_recycle_wt
return 1
fi
# 新产物承接能力复核(环境 DSN 交付;候选上、停服前):不支持即失败——绑定
# 保持可恢复,不降级启动。
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$staged/bin/agent-system"; then
c_err "[$ENV_NAME] 新产物不消费环境变量 DSN(AGENT_SYSTEM_DSN)——部署中止;旧绑定保留在 svc.env"
c_err " 恢复:换部署版本,或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config <file.json>)"
hist_event deploy fail "$(read_pid)" "新产物不支持环境 DSN($SRC_LABEL)"
rm -rf "$staged"
return 1
fi
svc_journal_write starting || return 1
prev_id="$(svc_release_cur_id)"
desired="stopped"; pid_alive "$(read_pid)" 2>/dev/null && desired="running"
svc_journal_write staging "new=$relid" "prev=$prev_id" "desired=$desired"
do_stop_quiet
svc_release_switch "$relid" || {
svc_journal_write blocked reason="current 指针切换失败"
hist_event deploy fail "$(read_pid)" "current 指针切换失败($SRC_LABEL)"
return 1
}
svc_journal_write switched "new=$relid" "prev=$prev_id" "desired=$desired"
c_ok "[$ENV_NAME] 已切换 release:releases/$relid(prev:${prev_id:-无})"
# 指针已切:重解析产物路径(env_config 解析时 current 可能尚不存在)。
ENV_BIN_DIR="$ENV_DIR/current/bin"
ENV_PLUGINS="$ENV_DIR/current/plugins"
ENV_BIN="$ENV_BIN_DIR/agent-system"
if ! agent_do_start; then
hist_event deploy fail "$(read_pid)" "启动失败($SRC_LABEL)"
svc_release_rollback || return 3
svc_rollback_deploy "新制品启动失败($SRC_LABEL)" || true
hist_event deploy fail "$(read_pid)" "启动失败已回退($SRC_LABEL)"
return 1
fi
case "$SRC_KINDSRC" in
rdm)
if [ "$RDM_UI_OK" != 1 ]; then
c_err "[$ENV_NAME] RDM 工作台未就位(ui-missing):缺少可伺服的 htdocs/"
hist_event deploy fail "$(read_pid)" "ui-missing(htdocs 未就位)"
return 1
fi
if ! deploy_rdm_companion_ui "$(rdm_companion_name "$ENV_NAME")" "$ENV_DIR/htdocs"; then
hist_event deploy fail "$(read_pid)" "ui-missing(伴生部署失败)"
# 顶层 htdocs → current/htdocs 符号链接:伴生经它伺服,指针切换自动换内容
# (legacy 实体目录内容已随领养保入 release,替换不丢)。
if [ -n "$srchtdocs" ]; then
if [ ! -L "$ENV_DIR/htdocs" ]; then
rm -rf "$ENV_DIR/htdocs"
ln -s current/htdocs "$ENV_DIR/htdocs"
fi
c_ok "[$ENV_NAME] RDM 工作台就位:releases/$relid/htdocs → $ENV_DIR/htdocs(伴生 $(rdm_companion_name "$ENV_NAME"))"
if ! deploy_rdm_companion_ui "$(rdm_companion_name "$ENV_NAME")" "$ENV_DIR/htdocs"; then
svc_rollback_deploy "RDM 伴生工作台部署失败(ui-missing)" || true
hist_event deploy fail "$(read_pid)" "ui-missing(伴生部署失败,已回退)"
return 1
fi
else
svc_rollback_deploy "RDM 源无 web/ 工作台(ui-missing)" || true
hist_event deploy fail "$(read_pid)" "ui-missing(源无 web/,已回退)"
return 1
fi
;;
esac
if ! assert_ui_ready; then
hist_event deploy fail "$(read_pid)" "ui-missing"
svc_release_rollback || return 3
svc_rollback_deploy "UI 探活失败(ui-missing)" || true
hist_event deploy fail "$(read_pid)" "ui-missing(已回退)"
return 1
fi
svc_journal_write verifying || return 1
svc_journal_write committed || return 1
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL"
svc_release_commit_schema "$relid"
svc_journal_clear
svc_release_gc
svc_deploy_recycle_wt
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL release=$relid"
return 0
}
@@ -2742,6 +3011,9 @@ agent_do_start() {
local exec_args
if ! agent_has_explicit_cfg && [ -n "$(svc_get exec)" ]; then
exec_args="$(svc_get exec)"
# U2:release 治理实例的持久化 exec 里 legacy plugins 路径跟随 current
#(指针切换后插件目录随 release 解析)。
[ -L "$ENV_DIR/current" ] && exec_args="${exec_args//$ENV_DIR\/plugins/$ENV_PLUGINS}"
c_info "[$ENV_NAME] 复用上次配置(svc.env exec=;显式参数可覆盖):$(mask_dsn "$exec_args")"
else
local args="" explicit_auth="${CLI_AUTH:-$(env_var "$ENV_NAME" AUTH)}"
@@ -2760,7 +3032,7 @@ agent_do_start() {
c_warn "[$ENV_NAME] 该产物不支持部分启动参数(旧版本二进制),已自动省略:${args:-<无>} → ${adjusted:-<无>}"
fi
args="$adjusted"
exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_DIR/plugins} $args $CLI_EXTRA"
exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_PLUGINS} $args $CLI_EXTRA"
exec_args="$(printf '%s' "$exec_args" | sed 's/[[:space:]]*$//')"
fi
SVC_EXEC="$exec_args"
@@ -4031,7 +4303,6 @@ cmd_set() {
# ---- 生命周期分派 ----
do_start() {
svc_reconcile_journal || return 1
if pid_alive "$(read_pid)"; then
c_warn "[$ENV_NAME] 已在运行 pid=$(read_pid)(如需换参数/新版本请用 deploy/restart)"
return 0
@@ -4993,6 +5264,10 @@ svc_lifecycle_main() {
case "$action" in
deploy|recover|start) svc_manifest_migrate || { svc_unlock; exit 1; } ;;
esac
# 对账必须发生在干净子进程里:父进程的调用方环境不得进入部署。
case "$action" in
deploy|recover) svc_deploy_journal_resolve || { svc_unlock; exit 1; } ;;
esac
local rc=0
case "$action" in
deploy)
@@ -5109,6 +5384,11 @@ main() {
case "$action" in
start|restart|deploy|init|adopt) svc_manifest_migrate || exit 1 ;;
esac
# U2:入口处理中断的切换(deploy.journal)。blocked 拒绝除 deploy 外的动作
#(显式换制品即解除路径);start/restart 触发回退判定。
case "$action" in
start|restart|deploy) svc_deploy_journal_resolve "$action" || exit 1 ;;
esac
case "$action" in
deploy)
case "$SVC_KIND" in