Merge remote-tracking branch 'origin/main' into feature/task-start-rootfix

This commit is contained in:
2026-09-30 15:28:27 +00:00
11 changed files with 374 additions and 13 deletions
+215 -3
View File
@@ -19,6 +19,7 @@
# scripts/svc.sh db set <name> [...] 等价于 set
# scripts/svc.sh commits [dev|release] [N] 可部署提交列表(默认 dev,N=20)
# scripts/svc.sh history [name] [N] 事件历史(默认全部,最近 20 条)
# scripts/svc.sh update [name] [--check] [--all] 检测远端最新并更新过时服务
# scripts/svc.sh help [主题] 分主题详细帮助
#
# ════════════════════ 部署来源(agent/static 的 deploy)════════════════════
@@ -118,6 +119,7 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po
scripts/svc.sh rm pg1 [--purge] 注销(--purge 连数据删除)
scripts/svc.sh commits [dev|release] [N] 可部署提交
scripts/svc.sh history [name] [N] 事件历史
scripts/svc.sh update [name] [--check] 检测远端最新并更新过时服务
帮助:--help 完整帮助 | help [主题] 分主题(model deploy lifecycle db dbops config examples safety)
文档:docs/svc.md
@@ -252,6 +254,17 @@ EOF
注销:
scripts/svc.sh rm <name> 停止并移除登记(目录/数据/历史保留)
scripts/svc.sh rm <name> --purge 连 runtime/<name>/ 整个目录一起删除
自动更新(仓库更新后一条命令刷新服务):
scripts/svc.sh update --check 只报告:每服务 当前来源 vs 最新(fetch 后比较)
scripts/svc.sh update 更新所有"过时且运行中"的服务
scripts/svc.sh update dev prod 点名更新(已停止的过时服务也会更新并启动)
scripts/svc.sh update --all 含已停止服务;--no-fetch 跳过网络
- local 源:自动 git pull --ff-only origin main + make sync 后重建
(仅当前分支==main 且工作树干净;否则跳过并警告,不碰未提交现场)
- dev@sha 源:直接部署远端 main 最新提交(隔离 worktree 构建,不碰主检出)
- release@源:部署 Releases 仓最新版本(sha256 校验)
- postgres / static --dir 不参与更新;每次更新写 history(update 事件)
EOF
return 0 ;;
db)
@@ -444,6 +457,7 @@ CLI_LBASE=""; CLI_LKEY=""; CLI_LNAME=""; CLI_TAVILY=""
CLI_FROM=""; CLI_PICK=""; CLI_DRYRUN=0
CLI_KIND=""; CLI_DIR=""; CLI_PURGE=0; CLI_REVEAL=0
CLI_APPLY=0; CLI_FORCE=0; CLI_ONLINE=0; CLI_FILE=""; CLI_OUT=""
CLI_CHECK=0; CLI_ALL=0; CLI_NOFETCH=0
CLI_DB_USER=""; CLI_DB_PASSWORD=""; CLI_DB_NAME=""; CLI_DB_AUTH=""
CLI_TAGS=()
@@ -503,7 +517,7 @@ addr_compose() {
}
# ---- 名称与端口 ----
RESERVED_ENVS=" status list add set db rm create dsn commits history deploy stop start restart logs help version "
RESERVED_ENVS=" status list add set db rm create dsn commits history update deploy stop start restart logs help version "
# valid_env NAME:服务名合法性(小写字母开头、a-z0-9-、≤16 位;保留字不可用)。
valid_env() {
@@ -932,12 +946,13 @@ materialize_dev_commit() {
git -C "$ROOT" worktree add --detach "$wt" "$full" >/dev/null || return 1
fi
c_info "[$ENV_NAME] 物化子模块组合(git submodule update --init --recursive;缺对象时需网络)"
git -C "$wt" submodule update --init --recursive || {
# stdout 只留给最后的 worktree 路径(调用方 $() 捕获)——子模块/构建输出全部走 stderr。
git -C "$wt" submodule update --init --recursive >&2 || {
c_err "[$ENV_NAME] 子模块物化失败(对象缺失时先 git -C $ROOT fetch origin)"
return 1
}
c_info "[$ENV_NAME] 构建(make $target:npm ci + 构建,需网络)"
make -C "$wt" "$target" || return 1
make -C "$wt" "$target" >&2 || return 1
echo "$wt"
}
@@ -2029,6 +2044,202 @@ cmd_history() {
done
}
# ---- 自动检索与更新 ----
# cmd_update [NAME...] [--check] [--all] [--no-fetch]:
# fetch 远端 → 逐服务比较登记 source 与最新(Dev 仓 origin/main / Releases 最新版本)→
# --check 只报告;否则更新"过时且运行中"的服务(点名或 --all 时含已停止)。
# local 源的自动 pull 受三重保护:当前分支==main + 工作树干净 + --ff-only。
cmd_update() {
local names=() a
while [ $# -gt 0 ]; do
case "$1" in
--check) CLI_CHECK=1; shift ;;
--all) CLI_ALL=1; shift ;;
--no-fetch) CLI_NOFETCH=1; shift ;;
--) shift; break ;;
-h|--help) usage; exit 0 ;;
-*) c_err "update 未知参数:$1(可用:--check --all --no-fetch)"; return 2 ;;
*) names+=("$1"); shift ;;
esac
done
local targets=() d e
if [ "${#names[@]}" -gt 0 ]; then
for a in ${names[@]+"${names[@]}"}; do
valid_env "$a" || { c_err "非法服务名:$a"; return 2; }
[ -f "$RUNTIME/$a/svc.env" ] || { c_err "[$a] 未登记(scripts/svc.sh list 查看)"; return 2; }
targets+=("$a")
done
else
for d in "$RUNTIME"/*/; do
[ -f "${d}svc.env" ] || continue
e="$(basename "$d")"
[ "$e" = ".worktrees" ] && continue
valid_env "$e" || continue
targets+=("$e")
done
fi
[ "${#targets[@]}" -gt 0 ] || { c_info "暂无已登记服务(db create / deploy 会登记)"; return 0; }
# fetch 远端(一次性;失败不致命,退化为本地缓存引用比较)。
local fetch_note=""
if [ "$CLI_NOFETCH" = 1 ]; then
fetch_note="基于本地缓存引用"
else
c_info "git fetch origin(检索远端更新)"
if git -C "$ROOT" fetch origin --quiet 2>/dev/null; then
:
else
fetch_note="fetch 失败,基于本地缓存引用"
c_warn "git fetch 失败——$fetch_note"
fi
fi
local latest_sha
latest_sha="$(git -C "$ROOT" rev-parse --verify --quiet origin/main || true)"
# Releases 最新版本(仅当存在 release 源服务时才访问 Releases 仓)。
local latest_rel="" need_rel=0 e_src
for e in ${targets[@]+"${targets[@]}"}; do
e_src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)"
case "$e_src" in release@*) need_rel=1 ;; esac
done
if [ "$need_rel" = 1 ]; then
releases_root
if [ "$CLI_NOFETCH" != 1 ]; then
git -C "$REL_ROOT" fetch --quiet 2>/dev/null || c_warn "Releases 仓 fetch 失败——基于本地缓存"
fi
local rel_head
rel_head="$(git -C "$REL_ROOT" rev-parse --verify --quiet HEAD || true)"
if [ -n "$rel_head" ]; then
latest_rel="$(git -C "$REL_ROOT" ls-tree --name-only "$rel_head:versions" 2>/dev/null | sort -V | tail -1 || true)"
fi
fi
# 逐服务分析:状态判定 + 收集可更新动作。
c_info "更新检测($fetch_note;root: $ROOT)"
printf ' %-12s %-20s %-12s %s\n' 服务 当前来源 最新 状态
local acts=() state to latest_disp="" updated=0 failed=0 skipped=0
for e in ${targets[@]+"${targets[@]}"}; do
local kind src cur st_running
kind="$(svc_kind "$e")"
src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)"
to=""
latest_disp=""
case "$kind" in
postgres)
state="不适用(数据库)" ;;
*)
case "$src" in
dir@*)
state="不适用(外部目录)" ;;
"")
state="来源未知(重新 deploy 后可参与更新)" ;;
local@*|dev@*)
cur="${src##*@}"
if [ -z "$latest_sha" ]; then
state="远端 main 未知"; latest_disp="-"
elif [ "${latest_sha:0:${#cur}}" = "$cur" ]; then
state="✓ 最新"; latest_disp="$(git -C "$ROOT" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")"
elif git -C "$ROOT" merge-base --is-ancestor "$cur" "$latest_sha" 2>/dev/null; then
to="$(git -C "$ROOT" rev-parse --short "$latest_sha" 2>/dev/null || true)"
latest_disp="$to"
state="可更新"
elif git -C "$ROOT" merge-base --is-ancestor "$latest_sha" "$cur" 2>/dev/null; then
state="领先远端 main(不动作)"; latest_disp="$(git -C "$ROOT" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")"
else
state="与远端 main 分叉(不自动更新)"; latest_disp="-"
fi
;;
release@*)
cur="${src#release@}"
latest_disp="${latest_rel:--}"
if [ -z "$latest_rel" ]; then
state="Releases 仓不可用"
elif [ "$cur" = "$latest_rel" ]; then
state="✓ 最新"
else
local ordered
ordered="$(printf '%s\n%s\n' "$cur" "$latest_rel" | sort -V | tail -1)"
if [ "$ordered" = "$latest_rel" ]; then
state="可更新"; to="$latest_rel"
else
state="领先 Releases(不动作)"
fi
fi
;;
*)
state="来源未知(重新 deploy 后可参与更新)" ;;
esac
;;
esac
printf ' %-12s %-20s %-12s %s\n' "$e" "${src:--}" "$latest_disp" "$state"
if [ "$state" = "可更新" ]; then
st_running="$(svc_pid_state "$e")"
if [ "${st_running%% *}" = alive ] || [ "$CLI_ALL" = 1 ] || [ "${#names[@]}" -gt 0 ]; then
acts+=("$e|$src|$to")
else
c_warn "[$e] 已停止且可更新——加 --all 或点名以更新(更新会启动服务)"
fi
fi
done
if [ "$CLI_CHECK" = 1 ]; then
c_info "--check:仅报告,未做任何变更"
return 0
fi
[ ${#acts[@]} -gt 0 ] || { c_ok "无需更新(全部最新或不适用)"; return 0; }
# 执行更新:逐服务走 deploy 全链路(复用持久化配置与标签)。
for a in ${acts[@]+"${acts[@]}"}; do
local name="${a%%|*}" rest="${a#*|}"
local from="${rest%%|*}" tov="${rest#*|}"
local deploy_from=""
env_config "$name"
case "$from" in
local@*)
local branch dirty
branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
if [ "$branch" != "main" ]; then
c_warn "[$name] 当前分支 $branch 非 main——跳过自动 pull(手动更新后重跑 update)"
skipped=$((skipped + 1)); continue
fi
dirty="$(git -C "$ROOT" status --porcelain | grep -v '^.. \.zcode/' || true)"
if [ -n "$dirty" ]; then
c_warn "[$name] 主检出工作树不干净——跳过(不碰未提交现场)"
skipped=$((skipped + 1)); continue
fi
c_info "[$name] git pull --ff-only origin main + make sync"
if ! git -C "$ROOT" pull --ff-only origin main >/dev/null 2>&1; then
c_err "[$name] 自动 pull 失败(可能本地 main 与远端分叉)——跳过,请手动处理"
hist_event update fail "$(read_pid)" "pull 失败(from=$from)"
failed=$((failed + 1)); continue
fi
if ! make -C "$ROOT" sync >/dev/null 2>&1; then
c_warn "[$name] make sync 失败——继续构建(若子模块缺失构建会失败)"
fi
deploy_from="local"
;;
dev@*) deploy_from="dev@$tov" ;;
release@*) deploy_from="release@$tov" ;;
esac
c_info "[$name] 更新:$from → $tov($deploy_from)"
hist_event update begin "$(read_pid)" "from=$from to=$tov"
CLI_FROM="$deploy_from"
if agent_do_deploy; then
hist_event update ok "$(read_pid)" "from=$from to=$tov"
c_ok "[$name] 已更新:$from → $tov"
updated=$((updated + 1))
else
hist_event update fail "$(read_pid)" "from=$from to=$tov(部署失败)"
c_err "[$name] 更新失败($from → $tov),其余服务继续"
failed=$((failed + 1))
fi
CLI_FROM=""
done
c_info "更新汇总:成功 $updated / 失败 $failed / 跳过 $skipped / 其余最新或不适用"
[ "$failed" -eq 0 ] && return 0 || return 1
}
# ---- main ----
main() {
@@ -2069,6 +2280,7 @@ main() {
rm) shift; cmd_rm "$@"; exit 0 ;;
commits) shift; cmd_commits "$@"; exit 0 ;;
history) shift; cmd_history "$@"; exit 0 ;;
update) shift; cmd_update "$@" || exit $?; exit 0 ;;
esac
ENV_NAME="$1"; shift
# 动作词可省略(省略 = deploy);flags 可直接跟随服务名。