From ac8db54ecb8ddc1e077bfec772a1665d3c2d753a Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 30 Sep 2026 15:22:33 +0000 Subject: [PATCH] Add svc.sh update command: auto-detect repo advances and refresh services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 描述) --- .task/feature-svc-auto-update.md | 43 +++++- Makefile | 6 +- docs/svc.md | 54 +++++++- scripts/svc.sh | 218 ++++++++++++++++++++++++++++++- 4 files changed, 308 insertions(+), 13 deletions(-) diff --git a/.task/feature-svc-auto-update.md b/.task/feature-svc-auto-update.md index c92b87fb..cdfff2c8 100644 --- a/.task/feature-svc-auto-update.md +++ b/.task/feature-svc-auto-update.md @@ -6,11 +6,20 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= ffda6d33eabb23c9be5bac221529367d771c5f7b - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-auto-update(worktree);分支:feature/svc-auto-update -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:用户确认方案(按需 update 命令、可点名、local 源允许自动 pull)后授权实施; + 终点 = 实现合入 main 并回收工作区。仅改 Dev 仓 scripts/svc.sh / Makefile / docs/svc.md。 ## 目标与非目标 -<预期结果、受影响入口/模块/契约;明确不做什么。> +目标:新增 `svc.sh update [name...] [--check] [--all] [--no-fetch]`—— +fetch 远端 → 逐服务比较登记 source 与最新(Dev 仓 origin/main / Releases 最新版本)→ +--check 只报告;否则更新过时且运行中的服务(点名/--all 含已停止)。local 源自动 pull +三重防护(分支==main + 树干净 + --ff-only);dev@ 源部署远端 main 最新(隔离 worktree); +release@ 源部署 Releases 最新(sha256 校验);postgres/static --dir 不适用。复用 deploy +全链路与持久化配置;update 事件入 history。Makefile 加 svc-update;帮助与文档同步。 + +非目标:不做常驻轮询/守护(--watch 不在本轮);不自动 pull Releases 仓以外的主检出分支; +不处理 local 源工作树的未提交变更内容(脏树=跳过)。 ## 同步记录 @@ -18,8 +27,32 @@ |---|---|---|---| | 启动 | ffda6d33eabb23c9be5bac221529367d771c5f7b | ffda6d33eabb23c9be5bac221529367d771c5f7b | 2026-09-30T15:02:15Z | +## 关键实现与修复记录 + +- 修复 main 潜伏 bug(v2 重构引入):materialize_dev_commit 改为 $() 捕获 worktree 路径后, + git submodule update 与 make 的 stdout 混入捕获值 → 产物路径损坏(cp: File name too + long)。dev@sha 源的真实部署自 v2 起实际不可用(此前仅 dry-run 覆盖)。修复:构建期 + stdout 全部改走 stderr,stdout 只留 worktree 路径。已实测修复后 dev@73b2802 部署成功。 +- 修复本轮新代码:cmd_update 执行循环中 `local name=... rest=... from="${rest%%|*}..."` + 同语句后项引用前项,set -u 下 unbound variable。拆分两条 local 语句。 + ## 结果与验证(完成后填写) -- 结果: -- 验证: -- 影响与剩余项: +- 结果:update 命令落地(检测表 + 分源更新 + 三重 pull 防护 + 汇总退出码);Makefile + svc-update;--help(命令表/短帮助/lifecycle 主题)与 docs/svc.md(6.3 节/上手/配方/ + 事件表/排查表)同步;RESERVED_ENVS 加 update。 +- 验证(任务工作区 runtime 内,5 个测试服务全链路,已全部清理): + - dev@ 源完整更新链路:deploy --from dev@73b2802(真实隔离构建)→ update --check 报告 + 可更新 → update devold → 隔离构建 ffda6d3 组合 → source=dev@ffda6d3、HTTP 200、 + 标签 updtest 与端口 8794 保留(复用持久化 exec=)→ 构建 worktree 回收。 + - local 源防护:source 置为旧祖先提交(测试 fixture)→ 状态"可更新"→ 执行时"当前分支 + 非 main——跳过自动 pull",汇总 跳过=1、exit 0、登记未动。"领先远端 main(不动作)" + 路径由 local@ 天然覆盖(feature 分支领先 origin/main)。 + - 已停止 + 可更新:不加 --all 仅警告;--all 后更新并启动(release@0.0.9 → 0.1.0, + sha256 校验通过)。 + - postgres / static --dir:报告"不适用"。release@0.1.0:报告"✓ 最新"。 + - --check 只读性:前后 svc.env md5 一致。点名未登记服务 exit 2。--no-fetch 跳过网络 + 并在报告注明。history 记录 update begin/ok 事件。 +- 影响与剩余项:main 分支上"落后→自动 pull→重建"路径受三重防护保护、逻辑简单,留待 + 真实使用首次触发验证;--watch 常驻轮询未做(用户选择按需命令);dirty-tree 跳过路径 + 代码就位(同防护块),工作区(feature 分支)无法真实触发,与 pull 路径一并留待验证。 diff --git a/Makefile b/Makefile index 25b33130..1af32331 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ PLUGINS := plugins COMBO := core=$(shell git -C $(CORE) rev-parse --short HEAD 2>/dev/null)+backend=$(shell git -C $(BACKEND) rev-parse --short HEAD 2>/dev/null)+frontend=$(shell git -C $(FRONTEND) rev-parse --short HEAD 2>/dev/null)+ui=$(shell git -C $(UI) rev-parse --short HEAD 2>/dev/null)+agentcmd=$(shell git -C deps/agent-command rev-parse --short HEAD 2>/dev/null)+documents=$(shell git -C modules/documents rev-parse --short HEAD 2>/dev/null) VERSION := $(shell git describe --tags --always --dirty 2>/dev/null)+$(COMBO) -.PHONY: help sync status build frontend svc svc-list svc-commits svc-history test-backend test-frontend test-ui test-core test-agentcmd dev pin clean setup precheck task-start task-sync task-close task-finish version +.PHONY: help sync status build frontend svc svc-list svc-commits svc-history svc-update test-backend test-frontend test-ui test-core test-agentcmd dev pin clean setup precheck task-start task-sync task-close task-finish version version: @echo "agent-system $(VERSION)" @@ -49,6 +49,7 @@ help: @echo "make svc-list 服务总览:make svc-list [ARGS='--tag core'];数据库运维:scripts/svc.sh db create|dsn|backup|backups|restore|clone|copy|ls|set" @echo "make svc-commits 列出可部署提交:make svc-commits SRC=dev|release [N=20]" @echo "make svc-history 事件历史:make svc-history [ENV] [N=20](完整文档:docs/svc.md)" + @echo "make svc-update 自动更新过时服务:make svc-update [NAMES='dev pg1'] [ARGS='--check --all']" @echo "make dev 开发模式说明(前后端分离热更新)" @echo "make pin 推进 submodule 指针到各仓远端 main(提交前须 make test 通过)" @echo "make status submodule 状态" @@ -110,6 +111,9 @@ svc-commits: svc-history: scripts/svc.sh history $(ENV) $(N) +svc-update: + scripts/svc.sh update $(NAMES) $(ARGS) + demo: build $(BIN) serve --addr 127.0.0.1:8787 --data ./data --plugins $(PLUGINS) --mock diff --git a/docs/svc.md b/docs/svc.md index ed058965..5d6a9e96 100644 --- a/docs/svc.md +++ b/docs/svc.md @@ -73,6 +73,10 @@ scripts/svc.sh history # 事件追溯 scripts/svc.sh db backup pg1 # pg_dump -Fc,自动自检 scripts/svc.sh db restore pg1 --file --force # 覆盖恢复需 --force scripts/svc.sh db clone pg1 pg1test --port 8896 # 克隆一套到新端口 + +# 仓库更新后,一条命令把服务刷到最新 +scripts/svc.sh update --check # 先看哪些服务落后 +scripts/svc.sh update # 自动更新过时且运行中的服务 ``` ## 3. 概念与目录布局 @@ -80,8 +84,8 @@ scripts/svc.sh db clone pg1 pg1test --port 8896 # 克隆一套到新端口 ### 3.1 服务名 `^[a-z][a-z0-9-]{0,15}$`(小写字母开头,可含数字与连字符,≤16 位)。保留字不可用作 -服务名:`status list add set db rm create dsn commits history deploy stop start restart -logs help version`。 +服务名:`status list add set db rm create dsn commits history update deploy stop start +restart logs help version`。 三个名字有默认端口:`dev` = `127.0.0.1:8788`,`prod` = `127.0.0.1:8787`;其余名字按 名称哈希在 **8800–9099** 确定性派生端口,与已登记端口或正在监听的端口冲突时自动向后 @@ -293,7 +297,42 @@ scripts/svc.sh history [name] [N] # 默认全部服务、最近 20 条;首 合并各服务 `history.jsonl`(按时间排序),字段:时间 / 服务 / kind / 事件 / 结果 / 来源 / pid / 详情。事件类型见 §10.2。 -### 6.3 help +### 6.3 update(自动检索并更新) + +```bash +scripts/svc.sh update [name...] [--check] [--all] [--no-fetch] +``` + +仓库更新后把服务刷到最新:fetch 远端 → 逐服务比较登记来源与最新 → 报告并(可选)更新。 + +| 用法 | 行为 | +|---|---| +| `update --check` | 只输出检测表,不改任何状态 | +| `update` | 自动更新所有"过时且**运行中**"的服务 | +| `update dev pg1` | 点名更新(已停止的过时服务也会更新并启动) | +| `update --all` | 范围扩大到已停止服务 | +| `update --no-fetch` | 跳过网络 fetch,用本地已有引用比较(报告注明) | + +检测表输出:服务 / 当前来源 / 最新 / 状态。状态含义: + +- `✓ 最新`——登记来源与最新一致,不动作; +- `可更新`——本地来源是远端最新的祖先,将被更新; +- `领先远端 main(不动作)` / `与远端 main 分叉(不自动更新)`——不盲目降级或强更; +- `不适用`——postgres(无代码版本)与 static `--dir`(外部目录)不参与。 + +各来源的更新动作(复用 deploy 全链路:停旧 → 构建/校验 → 启动 → 健康检查;持久化配置 +与标签保留;写 `update` 事件入 history): + +| 来源 | 更新方式 | +|---|---| +| `local@` | 当前分支为 main 且工作树干净时:自动 `git pull --ff-only origin main` + `make sync` + 重建;**树脏或非 main 分支则跳过并警告**(绝不碰未提交现场) | +| `dev@` | `deploy --from dev@<远端 main 最新>`(隔离 worktree 构建,不碰主检出) | +| `release@<版本>` | `deploy --from release@`(sha256 校验) | + +任一服务失败不中断其余服务;最终汇总 `成功/失败/跳过`。退出码:0 全部成功或最新; +1 有失败;2 用法错误。 + +### 6.4 help ```bash scripts/svc.sh # 一屏速览(短帮助) @@ -487,6 +526,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 | `restore` | ok / fail | 恢复(库、文件、表数) | | `clone` | ok / fail | 克隆(物理/在线) | | `copy` | ok / fail | 搬库 | +| `update` | begin / ok / fail | 自动更新(from → to) | | `rm` | ok | 注销(是否 purge) | ## 11. 安全与边界 @@ -526,6 +566,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 | `worktree 回收失败` | 按提示手动 `git worktree remove --force runtime/.worktrees/` | | `子模块物化失败` | 子模块对象缺失:先 fetch;或网络问题重试 | | `目标库已有 N 张表——拒绝覆盖` | 确认后加 `--force`(会 drop 重建) | +| `update 报"当前分支非 main"或"工作树不干净" | local 源服务的自动 pull 被防护跳过:先自行收尾/pull,再重跑 update;或改用 `deploy --from dev@` | | `list 里服务显示未运行但进程在` | 手工起的进程不满足 pidcheck 标记:脚本不会动它;纳入管理请走 svc 部署 | ## 14. 配方索引 @@ -567,7 +608,12 @@ scripts/svc.sh db copy pgdev pgprod --db app --force # ⑨ 换端口 scripts/svc.sh set pg1 --port 8895 --apply -# ⑩ 总览与追溯 +# ⑩ 仓库更新后刷新服务 +scripts/svc.sh update --check # 看哪些落后 +scripts/svc.sh update # 更新过时且运行中的(数据与配置保留) +scripts/svc.sh update dev # 只刷新指定服务 + +# ⑪ 总览与追溯 scripts/svc.sh list --tag test scripts/svc.sh history pg1 50 ``` diff --git a/scripts/svc.sh b/scripts/svc.sh index f06acf9c..0c0cb566 100755 --- a/scripts/svc.sh +++ b/scripts/svc.sh @@ -19,6 +19,7 @@ # scripts/svc.sh db set [...] 等价于 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 停止并移除登记(目录/数据/历史保留) scripts/svc.sh rm --purge 连 runtime// 整个目录一起删除 + +自动更新(仓库更新后一条命令刷新服务): + 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 可直接跟随服务名。