diff --git a/docs/svc.md b/docs/svc.md index 5d6a9e96..bfd623f8 100644 --- a/docs/svc.md +++ b/docs/svc.md @@ -12,7 +12,8 @@ PostgreSQL 数据库(postgres)三类服务统一为"服务"模型——按 - **登记持久化**:所有状态落盘(`svc.env` 当前态 + `history.jsonl` 事件流),`list` / `history` 全部从文件重建,脚本无内存状态、跨会话可追溯。 - **不碰现场**:不改主检出 git 状态;只停"自己 pidfile + cmdline 双校验"通过的进程; - 端口冲突报错不抢;除 `--from local` 外的构建全部在隔离 worktree。 + 端口冲突报错不抢;除 `--from local` 外的构建全部在隔离 worktree(物化自独立部署 + 源仓,与当前工作区状态无关——脏树、任意分支均可部署/更新)。 分主题速查:`scripts/svc.sh help [model|deploy|lifecycle|db|dbops|config|examples|safety]`; 本文档与 `--help` 内容同源,以实现为准。 @@ -54,7 +55,8 @@ PostgreSQL 工具链定位顺序:`SVC_PG_BIN` 环境变量 → `pg_config --bi ## 2. 90 秒上手 ```bash -# 一套演示后端(构建当前工作区 → 启动 → 健康检查) +# 一套演示后端(缺省部署远端 main 最新:独立部署源仓物化构建 → 启动 → 健康检查; +# 与当前工作区状态无关。要构建当前工作区:--from local) scripts/svc.sh dev deploy --mock # 一个数据库 + 让后端连上它 @@ -74,7 +76,7 @@ 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 # 自动更新过时且运行中的服务 ``` @@ -215,9 +217,30 @@ scripts/svc.sh rm --purge # 连 runtime// 整个目录一起删 ## 5. 命令参考:部署来源 -`deploy --from` 支持三种来源(缺省 `local`),agent 与 static 共用: +`deploy --from` 支持三种来源(**缺省 = `dev` 远端最新**),agent 与 static 共用: -### 5.1 local —— 当前工作区 +### 5.1 dev(缺省)—— 远端最新 / 指定提交 + +``` +scripts/svc.sh deploy # 缺省 = dev 远端最新(origin/main) +--from dev # 同缺省 +--from dev@ # 指定提交(短/长 sha 均可) +``` + +部署源来自**独立部署源仓**:脚本自有的干净克隆,默认 `~/.cache/agent-system-svc/src` +(`SVC_SRC_DIR` 覆盖;首次使用自动从本仓 origin 克隆,可随时删除、下次自动重建)。 +dev 源的提交解析、`commits` 列举与构建 worktree 物化全部基于它——**更新与部署因此 +不依赖当前工作区状态**:主检出脏树、任意分支、甚至未 fetch 都不影响。 + +流程:源仓自动 fetch → `git worktree add --detach runtime/.worktrees/ ` → +递归 `git submodule update --init --recursive`(子模块精确落位该提交的 gitlink 组合)→ +`make build`/`make frontend` → 产物复制 → worktree 回收。全程不触碰当前工作区。 + +- 可用提交列表:`scripts/svc.sh commits dev [N]`(列源仓 origin/main,自动确保+fetch); +- 需要 npm/go 网络(构建在 worktree 内进行); +- worktree 回收失败会提示手动命令,不阻塞部署结果。 + +### 5.2 local —— 当前工作区(显式) ``` --from local @@ -225,23 +248,8 @@ scripts/svc.sh rm --purge # 连 runtime// 整个目录一起删 在主检出跑 `make build`(agent)或 `make frontend`(static),产物复制到服务目录。 **这是唯一会写主检出的路径**,写的都是 gitignore 目录(`bin/`、`node_modules/`、 -`modules/frontend/dist/`),不产生任何 git 状态变化。适合"把我现在的工作区跑起来"。 - -### 5.2 dev@\ —— Dev 仓提交 - -``` ---from dev # 当前 HEAD ---from dev@ # 指定提交(短/长 sha 均可) -``` - -流程:`git worktree add --detach runtime/.worktrees/ ` → 递归 -`git submodule update --init --recursive`(子模块精确落位该提交的 gitlink 组合)→ -`make build`/`make frontend` → 产物复制 → worktree 回收。全程不触碰当前工作区。 - -- 可用提交列表:`scripts/svc.sh commits dev [N]`; -- 只列本地已有对象;远端新提交先 `git -C <仓库> fetch`; -- 需要 npm/go 网络(构建在 worktree 内进行); -- worktree 回收失败会提示手动命令,不阻塞部署结果。 +`modules/frontend/dist/`),不产生任何 git 状态变化;也是**唯一依赖当前工作区状态** +的来源。适合"把我现在的工作区跑起来"。 ### 5.3 release@\<版本\> —— Releases 仓制品 @@ -264,8 +272,8 @@ scripts/svc.sh deploy --pick # 交互式编号选择 scripts/svc.sh deploy --dry-run # 只打印计划 ``` -`commits` 表格:dev 源标注 `[HEAD]` 与 `[已部署:<服务名>]`(从各服务 `svc.env source=` -汇总);release 源额外给出版本号与对应 Dev 提交。 +`commits` 表格:dev 源标注 `[最新]`(源仓 origin/main 顶)与 `[已部署:<服务名>]` +(从各服务 `svc.env source=` 汇总);release 源额外给出版本号与对应 Dev 提交。 ## 6. 命令参考:全局命令 @@ -303,7 +311,8 @@ scripts/svc.sh history [name] [N] # 默认全部服务、最近 20 条;首 scripts/svc.sh update [name...] [--check] [--all] [--no-fetch] ``` -仓库更新后把服务刷到最新:fetch 远端 → 逐服务比较登记来源与最新 → 报告并(可选)更新。 +把服务刷到远端最新:fetch 独立部署源仓 → 逐服务比较登记来源与最新 → 报告并(可选) +更新。**全程不依赖当前工作区状态**(主检出脏树、任意分支均可更新)。 | 用法 | 行为 | |---|---| @@ -317,7 +326,8 @@ scripts/svc.sh update [name...] [--check] [--all] [--no-fetch] - `✓ 最新`——登记来源与最新一致,不动作; - `可更新`——本地来源是远端最新的祖先,将被更新; -- `领先远端 main(不动作)` / `与远端 main 分叉(不自动更新)`——不盲目降级或强更; +- `领先远端 main(不动作)` / `与远端 main 分叉(不自动更新)` / + `提交不在远端(不自动更新)`——不盲目降级或强更; - `不适用`——postgres(无代码版本)与 static `--dir`(外部目录)不参与。 各来源的更新动作(复用 deploy 全链路:停旧 → 构建/校验 → 启动 → 健康检查;持久化配置 @@ -325,8 +335,7 @@ scripts/svc.sh update [name...] [--check] [--all] [--no-fetch] | 来源 | 更新方式 | |---|---| -| `local@` | 当前分支为 main 且工作树干净时:自动 `git pull --ff-only origin main` + `make sync` + 重建;**树脏或非 main 分支则跳过并警告**(绝不碰未提交现场) | -| `dev@` | `deploy --from dev@<远端 main 最新>`(隔离 worktree 构建,不碰主检出) | +| `local@` / `dev@` | 一律 `deploy --from dev@<远端 main 最新>`(独立部署源仓 + 隔离 worktree 构建;不 pull 主检出、不看工作树状态) | | `release@<版本>` | `deploy --from release@`(sha256 校验) | 任一服务失败不中断其余服务;最终汇总 `成功/失败/跳过`。退出码:0 全部成功或最新; @@ -479,6 +488,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 |---|---| | `SVC__ADDR/DATA/DSN/AUTH/MOCK` | 按服务覆盖(NAME 大写、`-`→`_`,如 `staging` → `SVC_STAGING_ADDR`) | | `SVC_RELEASES_DIR` | Releases 仓路径(默认 `../Agent-System-Releases`) | +| `SVC_SRC_DIR` | 独立部署源仓路径(默认 `~/.cache/agent-system-svc/src`) | | `SVC_PG_BIN` | PostgreSQL 工具链 bindir | | `SVC_MODEL_BASE/KEY/NAME` | 主模型(播种 `settings.models.chat`) | | `SVC_LIGHT_BASE/KEY/NAME` | 轻量模型 | @@ -531,9 +541,10 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 ## 11. 安全与边界 -- **写入边界**:只写 `runtime//` 与 `runtime/.worktrees/`;不改主检出 git 状态; - `--from local` 是唯一写主检出构建产物的路径(均 gitignore 目录);`rm --purge` 只删 - 已登记服务的 `runtime//`。 +- **写入边界**:只写 `runtime//`、`runtime/.worktrees/` 与独立部署源仓 + (`SVC_SRC_DIR`,默认 `~/.cache/agent-system-svc/src`,可随时删除重建);不改主检出 + git 状态;`--from local` 是唯一写主检出构建产物的路径(均 gitignore 目录),也是 + 唯一依赖当前工作区状态的来源;`rm --purge` 只删已登记服务的 `runtime//`。 - **进程边界**:只停"pidfile + cmdline 标记"双校验通过的进程;postgres 走 `pg_ctl -m fast`,失败退 SIGTERM,SIGKILL 不用于 postmaster;agent/static TERM 10s 后才 KILL。 - **端口边界**:被他人占用报错不抢;显式端口双重校验(登记 + 监听);派生端口避让。 @@ -562,18 +573,19 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 | `pg_ctl start 失败` | 看 pg.log 常见:端口被占、pgdata 权限(须 700)、postmaster 残留 | | `pidfile 指向的进程已不存在` | 脚本自动清理 pidfile;直接重新 start 即可 | | `sha256 校验失败` | 制品与 manifest 不符:不要部署;在 Releases 仓重新 build | -| `Dev 仓无此提交` | 本地无该对象:`git -C <仓库> fetch` 后重试 | -| `worktree 回收失败` | 按提示手动 `git worktree remove --force runtime/.worktrees/` | -| `子模块物化失败` | 子模块对象缺失:先 fetch;或网络问题重试 | +| `部署源仓无此提交` | 源仓已自动 fetch 仍无此对象:确认 sha 正确(`scripts/svc.sh commits dev`) | +| `worktree 回收失败` | 按提示手动 `git -C <部署源仓> worktree remove --force runtime/.worktrees/` | +| `子模块物化失败` | 网络或子模块对象问题:重试(部署源仓会自动 fetch);或检查 git 远端可达性 | | `目标库已有 N 张表——拒绝覆盖` | 确认后加 `--force`(会 drop 重建) | -| `update 报"当前分支非 main"或"工作树不干净" | local 源服务的自动 pull 被防护跳过:先自行收尾/pull,再重跑 update;或改用 `deploy --from dev@` | +| `提交不在远端(不自动更新)` | 该服务部署自未推送的本地提交(如 `--from local`):重新 `deploy`(缺省即远端最新)即可纳入更新轨道 | | `list 里服务显示未运行但进程在` | 手工起的进程不满足 pidcheck 标记:脚本不会动它;纳入管理请走 svc 部署 | ## 14. 配方索引 ```bash -# ① 演示后端 +# ① 演示后端(缺省部署远端 main 最新;--from local 构建当前工作区) scripts/svc.sh dev deploy --mock +scripts/svc.sh dev deploy --from local --mock # ② 建库 + 后端连库 scripts/svc.sh db create pg1 --port 8890 @@ -608,7 +620,7 @@ 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 # 只刷新指定服务 diff --git a/scripts/svc.sh b/scripts/svc.sh index 0c0cb566..b8815546 100755 --- a/scripts/svc.sh +++ b/scripts/svc.sh @@ -19,13 +19,15 @@ # 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 update [name] [--check] [--all] 检测远端最新并更新过时服务(与当前工作区状态无关) # scripts/svc.sh help [主题] 分主题详细帮助 # # ════════════════════ 部署来源(agent/static 的 deploy)════════════════════ +# (缺省)dev@远端最新 不指定 --from 时默认部署源仓 origin/main 最新提交—— +# 更新/部署不依赖当前工作区状态(脏树、任意分支均可) +# --from dev[@] 同缺省;@ 显式指定提交。物化与构建在独立部署源仓的 +# 隔离 worktree(runtime/.worktrees/)进行,不触碰当前工作区 # --from local 构建当前工作区(唯一写主检出构建产物的路径,均 gitignore) -# --from dev[@] 隔离 worktree(runtime/.worktrees/)物化该提交的 -# 子模块组合并构建,不触碰当前工作区 # --from release[@<版本|sha>] Releases 仓制品(sha256 对 versions//manifest.json 校验) # --pick [dev|release] 交互式编号选提交;--dry-run 只打印部署计划 # @@ -73,9 +75,10 @@ # make svc ENV=dev ARGS="--env .env.dev --port 9000" # # ════════════════════ 安全约束 ════════════════════ -# - 只写 runtime// 与 runtime/.worktrees/;不改主检出 git 状态、不动他人现场; +# - 只写 runtime//、runtime/.worktrees/ 与独立部署源仓(SVC_SRC_DIR,默认 +# ~/.cache/agent-system-svc/src,可随时删除重建);不改主检出 git 状态、不动他人现场; # --from local 是唯一写主检出构建产物(bin/、node_modules/、dist/,均 gitignore) -# 的路径,dev@sha/release/static 构建全部在隔离 worktree; +# 的路径,dev@sha/release/static 构建全部在隔离 worktree(自部署源仓物化); # - 只停自己启动的进程:pidfile + /proc//cmdline 标记双校验;postgres 走 # pg_ctl -m fast 优雅停机,SIGKILL 不用于 postmaster; # - 端口被非本脚本进程占用时报错并给出占用者,不抢不杀; @@ -92,6 +95,10 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RUNTIME="$ROOT/runtime" BIN="$ROOT/bin/agent-system" SVC_ENVFILE="" +# 独立部署源仓:脚本自有的干净克隆(默认 ~/.cache/agent-system-svc/src,SVC_SRC_DIR 覆盖)。 +# dev 源的解析/列举/worktree 物化全部基于它——更新与部署因此不依赖当前工作区状态 +# (脏树、任意分支均可);目录可随时删除,下次使用自动重建。 +SRC_DIR="${SVC_SRC_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/agent-system-svc/src}" # 提示信息走 stderr:stdout 只留给数据(提交表/服务表/DSN),便于 $( ) 捕获组合。 c_info() { printf '\033[1;34m[svc]\033[0m %s\n' "$*" >&2; } @@ -110,7 +117,7 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po 常用: scripts/svc.sh list [--tag T] 服务总览 - scripts/svc.sh deploy --from dev@ 按提交部署并启动(agent/static) + scripts/svc.sh deploy [--from dev@] 部署并启动(缺省=远端 main 最新) scripts/svc.sh start|stop|restart|logs [N] 生命周期与日志 scripts/svc.sh status 服务详情(版本/来源/DSN 等) scripts/svc.sh db create pg1 --port 8890 建数据库 @@ -119,7 +126,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] 检测远端最新并更新过时服务 + scripts/svc.sh update [name] [--check] 刷新过时服务到远端最新(不依赖工作区状态) 帮助:--help 完整帮助 | help [主题] 分主题(model deploy lifecycle db dbops config examples safety) 文档:docs/svc.md @@ -204,13 +211,18 @@ EOF 语法:scripts/svc.sh deploy [--kind agent|static] [--from 源] [参数...] -部署来源(--from,默认 local): +部署来源(--from,缺省=dev 远端最新): + (缺省)/ dev 部署独立部署源仓的 origin/main 最新提交——与当前工作区 + 状态无关(脏树、任意分支均可)。 + dev@ 显式指定提交,物化与构建同上。 + 两者统一流程:在独立部署源仓(SVC_SRC_DIR,默认 ~/.cache/agent-system-svc/src, + 首次使用自动从 origin 克隆)上 git worktree add --detach 出 + runtime/.worktrees/,递归物化子模块组合(精确落位 gitlink), + make build/frontend,产物复制后回收 worktree。全程不触碰当前工作区。 local 构建当前工作区(make build / make frontend),产物复制到 服务目录。注意:这是唯一会写主检出构建产物(bin/、 - node_modules/、dist/——均 gitignore)的路径。 - dev[@] 在隔离 worktree(runtime/.worktrees/)检出该提交, - 递归物化子模块组合(精确落位 gitlink),make build/frontend, - 产物复制后回收 worktree。子模块对象缺失时需先 git fetch。 + node_modules/、dist/——均 gitignore)的路径,也是唯一 + 依赖当前工作区状态的来源。 release[@<版本|sha>] 取 Releases 仓 build//agent-system(+plugins), sha256 对 versions//manifest.json 校验,不符即拒绝。 --pick [dev|release] 交互式编号选择(需 TTY;列表与提示走 stderr) @@ -255,14 +267,14 @@ 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 构建,不碰主检出) + - local@/dev@ 源:一律重新部署为 dev@<远端 main 最新>(独立部署源仓 + 隔离 + worktree 构建;主检出脏净、所在分支均不影响,也不 pull 主检出) + - 领先远端 main / 与远端分叉 / 提交不在远端:报告但不动作(不盲目降级) - release@源:部署 Releases 仓最新版本(sha256 校验) - postgres / static --dir 不参与更新;每次更新写 history(update 事件) EOF @@ -345,6 +357,7 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只 环境变量(X 为服务名大写,- → _): SVC_X_ADDR / SVC_X_DATA / SVC_X_DSN / SVC_X_AUTH / SVC_X_MOCK SVC_RELEASES_DIR Releases 仓路径(默认 ../Agent-System-Releases) + SVC_SRC_DIR 独立部署源仓路径(默认 ~/.cache/agent-system-svc/src) SVC_PG_BIN postgres 工具链 bindir(默认 pg_config --bindir 探测) SVC_MODEL_BASE/KEY/NAME 主模型(agent 启动健康后播种) SVC_LIGHT_BASE/KEY/NAME 轻量模型 @@ -362,8 +375,9 @@ EOF cat >&2 <<'EOF' 【examples】常见配方 -① 起一套演示后端(当前工作区构建): +① 起一套演示后端(缺省部署远端 main 最新;--from local 才构建当前工作区): scripts/svc.sh dev deploy --mock + scripts/svc.sh dev deploy --from local --mock ② 建库 + 后端连库: scripts/svc.sh db create pg1 --port 8890 scripts/svc.sh pg1 start @@ -401,9 +415,10 @@ EOF 【safety】安全约束与边界 写入边界: - - 只写 runtime// 与 runtime/.worktrees/(构建 worktree,用后即收); + - 只写 runtime//、runtime/.worktrees/(构建 worktree,用后即收)与独立 + 部署源仓(SVC_SRC_DIR,默认 ~/.cache/agent-system-svc/src,可随时删除重建); - 不改主检出 git 状态;--from local 是唯一写主检出构建产物(bin/、node_modules/、 - dist/——均 gitignore)的路径; + dist/——均 gitignore)的路径,也是唯一依赖当前工作区状态的来源; - rm --purge 只删 runtime//,且要求服务已登记。 进程边界: @@ -781,6 +796,31 @@ releases_root() { fi } +# svc_src_ready:确保独立部署源仓就绪(不存在则从 $ROOT 的 origin 首次克隆)并 fetch +# 远端;--no-fetch 跳过网络。同一次命令运行内幂等(首个调用定下 fetch 策略)。 +# 失败 return 1(无 origin 可克隆/克隆失败),调用方自行决定报错或降级。 +SRC_READY_DONE=0 +svc_src_ready() { + [ "$SRC_READY_DONE" = 1 ] && return 0 + if [ ! -d "$SRC_DIR/.git" ]; then + local url + url="$(git -C "$ROOT" remote get-url origin 2>/dev/null || true)" + [ -n "$url" ] || { c_err "无法确定 origin 地址($ROOT 无 origin 远端);可用 SVC_SRC_DIR 指向已有仓"; return 1; } + c_info "初始化独立部署源仓:$SRC_DIR(← origin;与当前工作区无关,可随时删除重建)" + mkdir -p "$(dirname "$SRC_DIR")" + if ! git clone --quiet "$url" "$SRC_DIR" >&2; then + c_err "部署源仓克隆失败:$url → $SRC_DIR" + return 1 + fi + fi + if [ "${1:-}" != "--no-fetch" ]; then + c_info "git fetch(独立部署源仓:$SRC_DIR)" + git -C "$SRC_DIR" fetch origin --quiet 2>/dev/null || c_warn "部署源仓 fetch 失败——基于本地缓存($SRC_DIR)" + fi + SRC_READY_DONE=1 + return 0 +} + deployed_envs() { local key="$1" d e s out="" for d in "$RUNTIME"/*/; do @@ -797,20 +837,21 @@ print_commit_list() { # SRC N PICK:提交表到 stdout;PICK=1 时填充 PICK local src="$1" count="$2" pick="$3" local sep=$'\037' if [ "$src" = dev ]; then - c_info "Dev 仓提交($ROOT;倒序取 $count 条)——部署:scripts/svc.sh deploy --from dev@" + svc_src_ready || { c_err "部署源仓不可用,无法列出可部署提交"; return 1; } + c_info "Dev 仓提交(独立部署源仓 $SRC_DIR 的 origin/main;倒序取 $count 条)——部署:scripts/svc.sh deploy --from dev@" printf ' %3s %-9s %-10s %s\n' '#' 'sha' '日期' '主题' - local head_short; head_short="$(git -C "$ROOT" rev-parse --short HEAD)" + local latest_short; latest_short="$(git -C "$SRC_DIR" rev-parse --short origin/main 2>/dev/null || true)" local n=0 h date subject marks envs while IFS="$sep" read -r h date subject; do [ -z "$h" ] && continue n=$((n + 1)) marks="" - [ "$h" = "$head_short" ] && marks="${marks}[HEAD]" + [ "$h" = "$latest_short" ] && marks="${marks}[最新]" envs="$(deployed_envs "dev@$h")" [ -n "$envs" ] && marks="${marks}[已部署:$envs]" printf ' %3d %-9s %-10s %s %s\n' "$n" "$h" "$date" "$subject" "$marks" [ "$pick" = 1 ] && PICK_OPTS+=("$h") - done < <(git -C "$ROOT" log -n "$count" --format="%h${sep}%ad${sep}%s" --date=short) + done < <(git -C "$SRC_DIR" log -n "$count" --format="%h${sep}%ad${sep}%s" --date=short origin/main) else c_info "Releases 仓提交($REL_ROOT;倒序取 $count 条)——部署:scripts/svc.sh deploy --from release@<版本>" printf ' %3s %-9s %-10s %-8s %-9s %s\n' '#' 'sha' '日期' '版本' 'dev提交' '主题' @@ -882,7 +923,7 @@ resolve_release() { # REF(空=最新) → REL_COMMIT/REL_VERSION/SRC_DEVCOMMIT } resolve_source() { # → SRC_KINDSRC(local|dev|release) + SRC_LABEL(+SRC_FULL/SRC_SHA/REL_*) - local spec="${CLI_FROM:-local}" + local spec="${CLI_FROM:-dev}" if [ -n "$CLI_PICK" ]; then [ "$CLI_PICK" = release ] && releases_root spec="$(pick_commit "$CLI_PICK")" @@ -893,16 +934,18 @@ resolve_source() { # → SRC_KINDSRC(local|dev|release) + SRC_LABEL(+SRC_FULL/ SRC_LABEL="local@$(git -C "$ROOT" rev-parse --short HEAD)" ;; dev|dev@*) - local ref="HEAD" + # 缺省 ref=origin/main(独立部署源仓的远端最新);显式 @ 亦可。 + local ref="origin/main" case "$spec" in dev@*) ref="${spec#dev@}" ;; esac + svc_src_ready || { c_err "部署源仓不可用——dev 源依赖它;或改用 --from local 构建当前工作区"; exit 2; } local full - full="$(git -C "$ROOT" rev-parse --verify --quiet "${ref}^{commit}")" || { - c_err "Dev 仓无此提交:$ref(若是远端新提交,先 git -C $ROOT fetch;可用项:scripts/svc.sh commits dev)" + full="$(git -C "$SRC_DIR" rev-parse --verify --quiet "${ref}^{commit}")" || { + c_err "部署源仓无此提交:$ref(已自动 fetch;可用项:scripts/svc.sh commits dev)" exit 2 } SRC_KINDSRC=dev SRC_FULL="$full" - SRC_SHA="$(git -C "$ROOT" rev-parse --short "$full")" + SRC_SHA="$(git -C "$SRC_DIR" rev-parse --short "$full")" SRC_LABEL="dev@$SRC_SHA" ;; release|release@*) @@ -942,13 +985,13 @@ materialize_dev_commit() { if [ -d "$wt" ]; then c_info "[$ENV_NAME] 复用已有构建 worktree:$wt" else - c_info "[$ENV_NAME] 物化 Dev 提交 $sha → 隔离 worktree(不触碰当前工作区)" - git -C "$ROOT" worktree add --detach "$wt" "$full" >/dev/null || return 1 + c_info "[$ENV_NAME] 物化 Dev 提交 $sha → 隔离 worktree(源:独立部署源仓 $SRC_DIR,不触碰当前工作区)" + git -C "$SRC_DIR" worktree add --detach "$wt" "$full" >/dev/null || return 1 fi c_info "[$ENV_NAME] 物化子模块组合(git submodule update --init --recursive;缺对象时需网络)" # stdout 只留给最后的 worktree 路径(调用方 $() 捕获)——子模块/构建输出全部走 stderr。 git -C "$wt" submodule update --init --recursive >&2 || { - c_err "[$ENV_NAME] 子模块物化失败(对象缺失时先 git -C $ROOT fetch origin)" + c_err "[$ENV_NAME] 子模块物化失败(网络或对象缺失;重试会自动 fetch 部署源仓)" return 1 } c_info "[$ENV_NAME] 构建(make $target:npm ci + 构建,需网络)" @@ -960,8 +1003,8 @@ materialize_dev_commit() { recycle_worktree() { local wt="$RUNTIME/.worktrees/$1" c_info "[$ENV_NAME] 回收构建 worktree" - git -C "$ROOT" worktree remove --force "$wt" >/dev/null 2>&1 \ - || c_warn "[$ENV_NAME] worktree 回收失败,可手动:git -C $ROOT worktree remove --force $wt" + git -C "$SRC_DIR" worktree remove --force "$wt" >/dev/null 2>&1 \ + || c_warn "[$ENV_NAME] worktree 回收失败,可手动:git -C $SRC_DIR worktree remove --force $wt" } materialize_release() { @@ -1027,7 +1070,7 @@ print_agent_plan() { printf ' 来源:%s(make build 当前工作区;唯一写主检出构建产物的路径)\n' "$SRC_LABEL" printf ' 产物:%s ← %s + %s/plugins\n' "$ENV_BIN" "$BIN" "$ROOT" ;; dev) - printf ' 来源:%s(完整提交 %s)\n' "$SRC_LABEL" "$SRC_FULL" + printf ' 来源:%s(完整提交 %s;独立部署源仓 %s)\n' "$SRC_LABEL" "$SRC_FULL" "$SRC_DIR" printf ' 物化:worktree %s(submodule update --init --recursive + make build)\n' "$RUNTIME/.worktrees/$SRC_SHA" ;; release) printf ' 来源:%s(Releases 提交 %s)\n' "$SRC_LABEL" "$(git -C "$REL_ROOT" rev-parse --short "$REL_COMMIT")" @@ -1229,8 +1272,8 @@ print_static_plan() { else case "$SRC_KINDSRC" in local) printf ' 来源:%s(make frontend → htdocs/)\n' "$SRC_LABEL" ;; - dev) printf ' 来源:%s(完整提交 %s)\n 物化:worktree %s(make frontend → htdocs/)\n' \ - "$SRC_LABEL" "$SRC_FULL" "$RUNTIME/.worktrees/$SRC_SHA" ;; + dev) printf ' 来源:%s(完整提交 %s;独立部署源仓 %s)\n 物化:worktree %s(make frontend → htdocs/)\n' \ + "$SRC_LABEL" "$SRC_FULL" "$SRC_DIR" "$RUNTIME/.worktrees/$SRC_SHA" ;; release) printf ' 来源:release —— 不支持(制品内嵌前端);请用 dev|local 或 --dir\n' ;; esac fi @@ -2047,9 +2090,10 @@ cmd_history() { # ---- 自动检索与更新 ---- # cmd_update [NAME...] [--check] [--all] [--no-fetch]: -# fetch 远端 → 逐服务比较登记 source 与最新(Dev 仓 origin/main / Releases 最新版本)→ +# fetch 独立部署源仓 → 逐服务比较登记 source 与远端 main 最新(Releases 最新版本)→ # --check 只报告;否则更新"过时且运行中"的服务(点名或 --all 时含已停止)。 -# local 源的自动 pull 受三重保护:当前分支==main + 工作树干净 + --ff-only。 +# local@/dev@ 源一律重新部署为 dev@<远端最新>(隔离 worktree 构建)——与主检出 +# 工作树状态完全无关(脏树、任意分支均可);领先/分叉/提交不在远端不动作。 cmd_update() { local names=() a while [ $# -gt 0 ]; do @@ -2081,28 +2125,21 @@ cmd_update() { 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 + # 按目标来源决定访问哪些仓:local@/dev@ 走独立部署源仓;release@ 走 Releases 仓。 + local latest_sha="" latest_rel="" need_rel=0 need_dev=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 + case "$e_src" in release@*) need_rel=1 ;; local@*|dev@*) need_dev=1 ;; esac done + local fetch_note="" src_args="" + [ "$CLI_NOFETCH" = 1 ] && { fetch_note="--no-fetch,基于本地缓存引用"; src_args="--no-fetch"; } + if [ "$need_dev" = 1 ]; then + if svc_src_ready $src_args; then + latest_sha="$(git -C "$SRC_DIR" rev-parse --verify --quiet origin/main || true)" + else + c_warn "部署源仓不可用——local/dev 源服务本轮无法检测与更新" + fi + fi if [ "$need_rel" = 1 ]; then releases_root if [ "$CLI_NOFETCH" != 1 ]; then @@ -2138,14 +2175,17 @@ cmd_update() { cur="${src##*@}" if [ -z "$latest_sha" ]; then state="远端 main 未知"; latest_disp="-" + elif ! git -C "$SRC_DIR" cat-file -e "${cur}^{commit}" 2>/dev/null; then + state="提交不在远端(不自动更新;重新 deploy 可纳入更新轨道)" + latest_disp="$(git -C "$SRC_DIR" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")" 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)" + state="✓ 最新"; latest_disp="$(git -C "$SRC_DIR" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")" + elif git -C "$SRC_DIR" merge-base --is-ancestor "$cur" "$latest_sha" 2>/dev/null; then + to="$(git -C "$SRC_DIR" 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 "-")" + elif git -C "$SRC_DIR" merge-base --is-ancestor "$latest_sha" "$cur" 2>/dev/null; then + state="领先远端 main(不动作)"; latest_disp="$(git -C "$SRC_DIR" rev-parse --short "$latest_sha" 2>/dev/null || echo "-")" else state="与远端 main 分叉(不自动更新)"; latest_disp="-" fi @@ -2195,32 +2235,11 @@ cmd_update() { local from="${rest%%|*}" tov="${rest#*|}" local deploy_from="" env_config "$name" + # local@/dev@ 源统一重新部署为 dev@<远端最新>:独立部署源仓 + 隔离 worktree 构建, + # 不 pull 主检出、不看工作树状态(脏树、任意分支均可更新)。 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" ;; + 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"