Add svc.sh update command: auto-detect repo advances and refresh services
Why: After the repository moves forward, updating a running service meant manually figuring out the new commit, rebuilding and redeploying by hand; the user asked for the tool to detect and update automatically. Evidence: - "update [name...] [--check] [--all] [--no-fetch]": fetches origin, compares each service's registered source against the latest (Dev repo origin/main for local@/dev@ sources, newest Releases version for release@), prints a per-service table (current / latest / state), and without --check refreshes outdated running services by reusing the full deploy pipeline (stop → build/verify → start → health check; persisted exec= config and tags survive; update events appended to history). - local@ services: auto git pull --ff-only origin main + make sync before rebuilding, guarded three ways (current branch == main, clean worktree, ff-only) — dirty trees or non-main branches are skipped with a warning, never touching uncommitted work. - dev@ sources go straight to the remote main commit via an isolated worktree; release@ sources deploy the newest Releases version with sha256 verification; postgres and static --dir services are reported as not applicable; sources that diverged from or lead the remote are never force-updated. - Verified end to end with five test services: dev@73b2802 → dev@ffda6d3 (real isolated build, HTTP 200, tag/port preserved, worktree recycled), non-main-branch skip, stopped-service warning vs --all, release sha256-verified update, --check read-only (svc.env checksums unchanged), unregistered name exits 2. - Fixed en route: a latent bug from the v2 refactor — materialize_dev_commit leaked submodule/make stdout into the captured worktree path, breaking every real dev@sha deploy since (cp: File name too long); build output now goes to stderr. Also fixed an unbound-variable crash in the update loop (local statement self-reference under set -u). Impact: New read-mostly operational command; no behavior change to existing commands except dev@sha deploys now actually working end to end. Makefile gains svc-update; --help and docs/svc.md document the command. Refs: .task/feature-svc-auto-update.md(任务卡=PR 描述)
This commit is contained in:
+215
-3
@@ -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 可直接跟随服务名。
|
||||
|
||||
Reference in New Issue
Block a user