Compare commits

...
Author SHA1 Message Date
wangbo 2db18f5804 chore(deploy): 补充前端正式产物的测试验收入口 2026-10-14 12:09:37 +08:00
wangbo ef471e2106 chore(deploy): 提供保留现有配置的后端测试镜像入口 2026-10-14 11:43:55 +08:00
wangbo bf0ac6db54 fix(deploy): 按服务读取镜像定义避免依赖镜像误判 2026-10-14 11:24:13 +08:00
easyai03 9a801ea3d5 fix(release): 指定环境版本并仅人工提升稳定版
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
日常更新现在将已选应用的固定版本写入服务器 .env,通过仓库 Compose 变量解析镜像。Bash/PowerShell 拒绝浮动版本,并在 pull/up 前检查有效镜像与指定 tag 一致;只更新目标服务,核心单独更新保留其他版本,兼容 VERSION 继承值。Compose 变更仅在仓库维护,服务器仍由更新/迁移脚本同步,不手工编辑。

稳定版独立入口必须提供同一个完整 version/confirm-version;所有固定产物 digest/架构先核验,然后按 digest 补 latest/latest-no-ssr,全部读回后才生成稳定清单。失败尝试恢复旧稳定指针。日常版本记录使用 system-update/releases/<版本>.json;补录现有 .14.2 已验收来源,没有改变现有稳定清单。

验证:稳定提升6项通过;真实 Bash/PowerShell入口(本机隔离命令传输)28.845秒通过,含单后端、核心、独立网关/Updater、浮动/覆盖拒绝、真实 Git pull/旧配置迁移及实际 Compose 解析;环境迁移通过,旧部署迁移0.894秒通过。未连接或更新生产/测试服务器,未执行真实镜像构建/标签推送。
2026-10-14 08:40:48 +08:00
easyai03 6691ac34e0 fix(release): 补齐前端变体的人工稳定标签映射
Test deployment public URL (Linux) / test-linux (pull_request) Canceled after 0s
Test start.ps1 (Windows) / test-windows (pull_request) Canceled after 0s
2026-10-14 08:39:50 +08:00
easyai03 8c1eadc5ea fix(release): 环境固定版本并拆分人工稳定版提升
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
Test deployment public URL (Linux) / test-linux (pull_request) Canceled after 0s
Test start.ps1 (Windows) / test-windows (pull_request) Canceled after 0s
2026-10-14 08:35:04 +08:00
wangbo 5fa19eff08 Merge pull request 'fix(release): 同步公开搜索组织范围修复稳定清单' (#8) from codex/search-scope-release-20261014 into main
fix(release): 发布公开搜索组织范围修复正式版本
2026-10-14 01:00:11 +08:00
wangbo 30c5d31787 fix(release): 同步公开搜索组织范围修复稳定清单 2026-10-14 00:59:32 +08:00
wangbo e337f6b7fb Merge pull request 'fix(release): 发布历史搜索递归溢出修复正式清单' (#7) from codex/search-recursion-release-20261014 into main
fix(release): 同步历史搜索递归修复正式稳定清单
2026-10-14 00:48:42 +08:00
wangbo 15a36fd085 fix(release): 发布历史搜索递归溢出修复正式清单 2026-10-14 00:47:14 +08:00
wangbo b77f27f6d5 Merge pull request 'chore(release): 发布 3.29.0.20261013.6 段错误修复清单' (#6) from codex/native-stack-release-20261013 into main
chore(release): 合并主后端段错误修复正式发布清单
2026-10-14 00:05:49 +08:00
wangbo 25e38b4441 chore(release): 发布主后端段错误修复正式镜像清单 2026-10-14 00:04:46 +08:00
easyai03 111ccab266 Merge pull request 'chore(release): 发布 3.29.0.20261013.5 镜像清单' (#5) from codex/runninghub-release-20261013-deploy into main 2026-10-13 20:01:07 +08:00
easyai03 23c9a989d4 chore(release): 更新正式镜像清单至 3.29.0.20261013.5 2026-10-13 19:59:14 +08:00
wangbo e082e24be7 Merge pull request 'chore(release): 发布后端性能修复清单并复用前端镜像' (#4) from codex/expense-index-followup-release-20261013 into main 2026-10-13 18:48:22 +08:00
wangbo ede392c310 chore(release): 更新后端性能修复镜像清单并复用前端产物 2026-10-13 18:47:00 +08:00
wangbo 7114eeee01 chore(release): 发布费用列表保护与查询性能修复清单 2026-10-13 13:24:32 +08:00
wangbo 85399bb185 chore(release): 发布费用列表保护与查询性能修复清单 2026-10-13 13:21:31 +08:00
wangbo 79e6bdf65e chore(release): 发布主站索引与媒体上下文修复清单 2026-10-13 10:54:11 +08:00
easyai03 ffaa01d434 chore(release): 发布 3.29.0.20261013 稳定清单并标注凭据回滚边界 2026-10-13 03:25:18 +08:00
easyai03 4cff2dcc58 feat(deploy): 合并凭据加密迁移及密钥备份恢复 (#2)
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
完成 API Key 加密、旧部署兼容及迁移支持。

本地验收通过;宁波服务器验收尚未执行。
任务:credential-ningbo-20261012
2026-10-13 00:37:53 +08:00
17 changed files with 577 additions and 56 deletions
+10 -2
View File
@@ -259,7 +259,15 @@ STATIC_CDN_OSS_USE_SSL=true
STATIC_CDN_FORCE_UPLOAD=false STATIC_CDN_FORCE_UPLOAD=false
# ========== 15. 版本 ========== # ========== 15. 版本 ==========
VERSION=latest VERSION=3.29.0.20261014.2
# 应用镜像只使用完整版本号;latest/latest-no-ssr 仅在人工指定版本后提升为稳定版。
# 组件未单独填写时继承 VERSION;update.sh/update.ps1 会持久化实际选中的组件版本。
SERVER_VERSION=
WEB_VERSION=
WS_GATEWAY_VERSION=
UPDATER_VERSION=
ASG_VERSION=
AMS_VERSION=
# ========== 15.1 在线更新 ========== # ========== 15.1 在线更新 ==========
# 新安装默认使用可查询任务模式;存量环境执行 update.sh 后会自动补齐缺失配置。 # 新安装默认使用可查询任务模式;存量环境执行 update.sh 后会自动补齐缺失配置。
@@ -294,7 +302,7 @@ EASYAI_DEPLOYMENT_SCHEMA_VERSION=2
# ========== 15.2 3D 模型轻量转换 ========== # ========== 15.2 3D 模型轻量转换 ==========
# update 脚本自动补齐缺失配置并拉取独立容器,无需手动修改线上配置。 # update 脚本自动补齐缺失配置并拉取独立容器,无需手动修改线上配置。
# 可填写已发布版本号固定镜像;latest 跟随正式转换镜像。 # 可填写已发布版本号固定镜像;latest 跟随正式转换镜像。
MODEL_CONVERSION_WORKER_VERSION=latest MODEL_CONVERSION_WORKER_VERSION=
# 安装/升级时自动生成,API 和 Worker 共用。已有强令牌不会重新生成。 # 安装/升级时自动生成,API 和 Worker 共用。已有强令牌不会重新生成。
MODEL_CONVERSION_WORKER_TOKEN= MODEL_CONVERSION_WORKER_TOKEN=
# 付费通道默认关闭;自建失败不会自动扣费转入其他通道。 # 付费通道默认关闭;自建失败不会自动扣费转入其他通道。
+9 -4
View File
@@ -175,6 +175,10 @@ mv easyai easyai2
5. 完成上述迁移后,后续若有新的配置文件或版本更新,可直接在新的 `easyai` 目录中执行 `./update.sh` 进行更新。 5. 完成上述迁移后,后续若有新的配置文件或版本更新,可直接在新的 `easyai` 目录中执行 `./update.sh` 进行更新。
日常发布只构建并推送完整版本标签,线上与测试环境均通过 `update.sh` / `update.ps1` 选择指定版本。`UPDATE_RELEASE_VERSION` 统一指定已选组件;`UPDATE_SERVER_VERSION`、`UPDATE_WEB_VERSION`、`UPDATE_GATEWAY_VERSION`、`UPDATE_WORKER_VERSION` 可分别指定已验证版本。脚本持久化 `SERVER_VERSION`、`WEB_VERSION`、`WS_GATEWAY_VERSION`、`MODEL_CONVERSION_WORKER_VERSION` 等到服务器 `.env`,保留非目标的原版本,拉取前核对有效 Compose 确实使用这些固定 tag。配置只能由已提交的部署仓库与迁移脚本更新,不手工修改服务器 `.env` 或 Compose/覆盖文件。
`latest` / `latest-no-ssr` 仅作为人工稳定版指针。只有用户明确指定版本转稳定版后才运行 `node scripts/promote-stable-release.mjs --version <版本> --confirm-version <同一版本>`(先 `--dry-run`),按固定产物 digest 复制稳定别名、读回并生成稳定清单,再审查和提交;日常 deploy 不调用此入口,也不修改稳定清单。该操作不重编业务镜像、不改业务环境版本。版本记录须包括独立 SSR/no-SSR 和实际使用的必要配套产物。
update.sh 脚本用于自动更新 EasyAI 应用,包含以下功能: update.sh 脚本用于自动更新 EasyAI 应用,包含以下功能:
- **拉取整个仓库**:执行 `git pull` 获取最新代码(docker-compose.yml、start.sh、.env.*.sample 等全部文件) - **拉取整个仓库**:执行 `git pull` 获取最新代码(docker-compose.yml、start.sh、.env.*.sample 等全部文件)
- 自动补齐缺失的环境配置文件(.env、.env.tools、.env.ASG、.env.AMS,从 .sample 生成且不覆盖已有文件) - 自动补齐缺失的环境配置文件(.env、.env.tools、.env.ASG、.env.AMS,从 .sample 生成且不覆盖已有文件)
@@ -190,7 +194,7 @@ update.sh 脚本用于自动更新 EasyAI 应用,包含以下功能:
更新执行器 `easyai-updater` 由 Watchtower 默认每 24 小时检查更新,同时由宿主机定时任务每六小时直接检查更新,提供两条独立维护路径。它的任务状态保存在宿主机 `data/update-state` 中,自身更新或意外重启后会继续恢复未完成任务;Watchtower 不再接管三个核心服务,因此页面任务结果不会与后台静默替换相互冲突。 更新执行器 `easyai-updater` 由 Watchtower 默认每 24 小时检查更新,同时由宿主机定时任务每六小时直接检查更新,提供两条独立维护路径。它的任务状态保存在宿主机 `data/update-state` 中,自身更新或意外重启后会继续恢复未完成任务;Watchtower 不再接管三个核心服务,因此页面任务结果不会与后台静默替换相互冲突。
受控更新默认直接获取部署仓库 `main` 分支的 `system-update/release-manifest.json`,无需先在服务器执行 `git pull`。页面检测、手动更新和自动更新使用同一发布清单;任务开始后将清单保存在任务记录中,服务重启恢复时继续使用原目标。发布新正式镜像时必须同步发布清单中的不可变摘要、镜像大小和版本标识,不能只修改官方站运行版本或推送 `latest` 镜像。 受控更新默认直接获取部署仓库 `main` 分支的 `system-update/release-manifest.json`,无需先在服务器执行 `git pull`。页面检测、手动更新和自动更新使用同一发布清单;任务开始后将清单保存在任务记录中,服务重启恢复时继续使用原目标。该清单只表示人工确认的稳定版。日常发布只推完整版本标签并保存 `system-update/releases/<版本>.json`;只有人类明确指定某个版本转稳定版后,才更新稳定清单与 `latest` / `latest-no-ssr`。线上或测试环境的普通版本发布不会自动推广给其他安装。
发布源不可用时会提示暂时无法检查更新,并保留当前服务;不会用历史清单宣称已是最新。`UPDATE_MANIFEST_URL` 可配置私有发布源,留空时使用官方部署仓库。离线部署可显式设置 `UPDATE_MANIFEST_SOURCE=local`,此时读取 Compose 只读挂载的本地清单;需要自行维护该清单。旧部署中的 `UPDATE_MANIFEST_FILE` 路径覆盖不再影响默认远程模式。 发布源不可用时会提示暂时无法检查更新,并保留当前服务;不会用历史清单宣称已是最新。`UPDATE_MANIFEST_URL` 可配置私有发布源,留空时使用官方部署仓库。离线部署可显式设置 `UPDATE_MANIFEST_SOURCE=local`,此时读取 Compose 只读挂载的本地清单;需要自行维护该清单。旧部署中的 `UPDATE_MANIFEST_FILE` 路径覆盖不再影响默认远程模式。
@@ -217,7 +221,7 @@ Watchtower 默认使用 `latest`。`start.sh/update.sh` 自动安装宿主机 cr
两个页面入口共用计时和步骤窗口。updater 保存阶段完成时间到 `data/update-state/jobs`;无 updater 时后端保存任务和单任务锁到 MongoDB。刷新或重新登录后按站点与账号恢复任务查询。请求超时或服务断开不会自动创建另一项更新,旧接口无法读取的阶段会明确提示。后端重启时页面保留进度,提醒在 3–5 分钟后刷新检查版本;只有确认目标版本和服务恢复才显示完成。 两个页面入口共用计时和步骤窗口。updater 保存阶段完成时间到 `data/update-state/jobs`;无 updater 时后端保存任务和单任务锁到 MongoDB。刷新或重新登录后按站点与账号恢复任务查询。请求超时或服务断开不会自动创建另一项更新,旧接口无法读取的阶段会明确提示。后端重启时页面保留进度,提醒在 3–5 分钟后刷新检查版本;只有确认目标版本和服务恢复才显示完成。
正式版本检测默认仍使用原来的 `https://openapi.51easyai.com`。测试验收可在后端设置 `APP_VERSION_CHECK_BASE_URL`,该地址应提供与正式地址相同格式的 `/app/version` 响应;正式发布前必须撤销测试覆盖。候选镜像用版本标签发布,验收后才将相同摘要提升为 `latest`。源码、镜像、环境迁移、三种实际更新验收和回退证据记录在 `system-update/releases/`,其中不得包含令牌或登录凭据。 正式版本检测默认仍使用原来的 `https://openapi.51easyai.com`。测试验收可在后端设置 `APP_VERSION_CHECK_BASE_URL`,该地址应提供与正式地址相同格式的 `/app/version` 响应;正式发布前必须撤销测试覆盖。所有日常镜像使用完整版本标签;验收成功也不会自动提升 `latest`,只有人类明确指定版本转稳定版后才按既有摘要提升。源码、镜像、环境迁移、三种实际更新验收和回退证据记录在 `system-update/releases/`,其中不得包含令牌或登录凭据。
以后新增必需环境变量时,在 `scripts/update-env.d/` 增加新的编号 `.env` 文件即可,不需要继续把写入逻辑堆进 `update.sh`。迁移文件支持 `KEY=value`(仅在缺失或空值时写入)、`KEY+=item`(确保逗号列表包含该项),以及 `@skip-if KEY=value`(明确保留某种旧模式);所有迁移每次都会执行并保持幂等。迁移失败会在拉取镜像前终止,避免使用半完成配置启动服务。 以后新增必需环境变量时,在 `scripts/update-env.d/` 增加新的编号 `.env` 文件即可,不需要继续把写入逻辑堆进 `update.sh`。迁移文件支持 `KEY=value`(仅在缺失或空值时写入)、`KEY+=item`(确保逗号列表包含该项),以及 `@skip-if KEY=value`(明确保留某种旧模式);所有迁移每次都会执行并保持幂等。迁移失败会在拉取镜像前终止,避免使用半完成配置启动服务。
@@ -257,12 +261,13 @@ UPDATE_SKIP_REPO_UPDATE=true ./update.sh
```bash ```bash
UPDATE_MODE=full UPDATE_NON_INTERACTIVE=1 \ UPDATE_MODE=full UPDATE_NON_INTERACTIVE=1 \
UPDATE_SERVICES='easyai-server easyai-web model-conversion-worker' ./update.sh UPDATE_SERVICES='easyai-server easyai-web model-conversion-worker' \
UPDATE_RELEASE_VERSION="<指定完整版本号>" ./update.sh
``` ```
脚本拉取部署仓库后会切换到新版脚本,补齐缺失的转换配置;已有令牌、端口、数据卷和显式付费选择保持原值。指定 `UPDATE_SERVICES` 时只重建这些服务,不重建数据库等依赖;服务名称有误会在拉取镜像前终止。普通更新不指定该变量时保留原来的整组更新行为。转换容器只加入业务网络,不开放主机端口,不连接沙箱网络,不挂载 Docker socket,也不读取整个生产 `.env`。默认限额为 2 CPU、4 GiB、并发 1、单任务 300 秒、临时空间 2 GiB;原模型始终保留,轻量转换失败可使用原模型。 脚本拉取部署仓库后会切换到新版脚本,补齐缺失的转换配置;已有令牌、端口、数据卷和显式付费选择保持原值。指定 `UPDATE_SERVICES` 时只重建这些服务,不重建数据库等依赖;服务名称有误会在拉取镜像前终止。普通更新不指定该变量时保留原来的整组更新行为。转换容器只加入业务网络,不开放主机端口,不连接沙箱网络,不挂载 Docker socket,也不读取整个生产 `.env`。默认限额为 2 CPU、4 GiB、并发 1、单任务 300 秒、临时空间 2 GiB;原模型始终保留,轻量转换失败可使用原模型。
`MODEL_CONVERSION_WORKER_VERSION` 可以固定已发布的版本;默认 `latest`。混元付费通道默认关闭,自建转换失败不自动切换到付费服务。已有 updater 的页面更新仍按稳定清单管理三个核心服务;转换 Worker 的首次安装和配置升级使用上述 `update` 脚本完成。 `MODEL_CONVERSION_WORKER_VERSION` 使用已发布的固定版本,未独立填写时继承 `VERSION`;更新脚本将目标组件版本持久化到 `.env`,应用更新不接受 `latest` 或其他浮动标签。混元付费通道默认关闭,自建转换失败不自动切换到付费服务。已有 updater 的页面更新仍按稳定清单管理三个核心服务;转换 Worker 的首次安装和配置升级使用上述 `update` 脚本完成。
测试候选版本时使用 `UPDATE_RELEASE_VERSION=3.23.0.20261003`,由更新脚本固定核心服务和转换 Worker 的版本,无需改动远端 `.env` 或移动 `latest` 标签。版本号不合法时在修改配置前终止。 测试候选版本时使用 `UPDATE_RELEASE_VERSION=3.23.0.20261003`,由更新脚本固定核心服务和转换 Worker 的版本,无需改动远端 `.env` 或移动 `latest` 标签。版本号不合法时在修改配置前终止。
+5
View File
@@ -0,0 +1,5 @@
# 自有测试环境的后端候选验收。追加到既有 COMPOSE_FILE,仅选择主后端镜像。
# EASYAI_TEST_SERVER_IMAGE 必须为已发布并核验的完整版本镜像。
services:
easyai-server:
image: ${EASYAI_TEST_SERVER_IMAGE:?EASYAI_TEST_SERVER_IMAGE is required}
+5
View File
@@ -0,0 +1,5 @@
# 自有测试环境与已发布前端产物配对验收,仅选择前端镜像。
# 不参与生产或客户的默认更新配置。
services:
easyai-web:
image: ${EASYAI_TEST_WEB_IMAGE:?EASYAI_TEST_WEB_IMAGE is required}
+7 -7
View File
@@ -5,7 +5,7 @@ services:
easyai-web: easyai-web:
container_name: easyai-web container_name: easyai-web
#更新修改冒号后面的版本号 #更新修改冒号后面的版本号
image: registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai:${VERSION} image: registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai:${WEB_VERSION:-${VERSION}}
#端口冲突时,只需要修改前面的端口比如修改为,3011:3010 #端口冲突时,只需要修改前面的端口比如修改为,3011:3010
labels: labels:
- "com.centurylinklabs.watchtower.enable=${WATCHTOWER_CORE_UPDATE_ENABLED:-true}" - "com.centurylinklabs.watchtower.enable=${WATCHTOWER_CORE_UPDATE_ENABLED:-true}"
@@ -58,7 +58,7 @@ services:
easyai-server: easyai-server:
container_name: easyai-server container_name: easyai-server
# 阿里云镜像地址 # 阿里云镜像地址
image: registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server:${VERSION} # 阿里云 image: registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server:${SERVER_VERSION:-${VERSION}} # 阿里云
labels: labels:
- "com.centurylinklabs.watchtower.enable=${WATCHTOWER_CORE_UPDATE_ENABLED:-true}" - "com.centurylinklabs.watchtower.enable=${WATCHTOWER_CORE_UPDATE_ENABLED:-true}"
- "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}" - "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}"
@@ -170,7 +170,7 @@ services:
max-file: "10" max-file: "10"
model-conversion-worker: model-conversion-worker:
container_name: easyai-model-conversion-worker container_name: easyai-model-conversion-worker
image: registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker:${MODEL_CONVERSION_WORKER_VERSION:-latest} image: registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker:${MODEL_CONVERSION_WORKER_VERSION:-${VERSION}}
labels: labels:
- "com.centurylinklabs.watchtower.enable=${WATCHTOWER_CORE_UPDATE_ENABLED:-true}" - "com.centurylinklabs.watchtower.enable=${WATCHTOWER_CORE_UPDATE_ENABLED:-true}"
- "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}" - "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}"
@@ -224,7 +224,7 @@ services:
ws-gateway: ws-gateway:
container_name: easyai-wsgateway # websocket网关 container_name: easyai-wsgateway # websocket网关
# 阿里云镜像地址 # 阿里云镜像地址
image: registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway:${WS_GATEWAY_VERSION:-latest} # 阿里云 image: registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway:${WS_GATEWAY_VERSION:-${VERSION}} # 阿里云
labels: labels:
- "com.centurylinklabs.watchtower.enable=${WATCHTOWER_CORE_UPDATE_ENABLED:-true}" - "com.centurylinklabs.watchtower.enable=${WATCHTOWER_CORE_UPDATE_ENABLED:-true}"
- "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}" - "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}"
@@ -391,7 +391,7 @@ services:
max-size: "100m" max-size: "100m"
max-file: "10" max-file: "10"
easyai-updater: easyai-updater:
image: registry.cn-shanghai.aliyuncs.com/easyaigc/easyai-updater:${UPDATER_VERSION:-latest} image: registry.cn-shanghai.aliyuncs.com/easyaigc/easyai-updater:${UPDATER_VERSION:-${VERSION}}
container_name: easyai-updater container_name: easyai-updater
profiles: profiles:
- managed-update - managed-update
@@ -567,7 +567,7 @@ services:
max-file: "10" max-file: "10"
easyai-asg: easyai-asg:
container_name: easyai-asg container_name: easyai-asg
image: registry.cn-shanghai.aliyuncs.com/easyaigc/agent-governance:${ASG_VERSION:-latest} image: registry.cn-shanghai.aliyuncs.com/easyaigc/agent-governance:${ASG_VERSION:-${VERSION}}
labels: labels:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
- "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}" - "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}"
@@ -599,7 +599,7 @@ services:
memory: 128MB memory: 128MB
agent-memory: agent-memory:
container_name: agent-memory container_name: agent-memory
image: registry.cn-shanghai.aliyuncs.com/easyaigc/agent-memory:${AMS_VERSION:-latest} image: registry.cn-shanghai.aliyuncs.com/easyaigc/agent-memory:${AMS_VERSION:-${VERSION}}
labels: labels:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
- "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}" - "com.centurylinklabs.watchtower.scope=${WATCHTOWER_SCOPE:-}"
@@ -0,0 +1,69 @@
function Get-ApplicationVersionPlan {
param([string]$Path)
$content = Get-Content $Path -Raw -Encoding UTF8
$selection = [string]$env:UPDATE_SERVICES
$selected = @($selection.Trim() -split '\s+' | Where-Object { $_ })
$mapping = @(
@('easyai-server','SERVER_VERSION','UPDATE_SERVER_VERSION','registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server'),
@('easyai-web','WEB_VERSION','UPDATE_WEB_VERSION','registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai'),
@('ws-gateway','WS_GATEWAY_VERSION','UPDATE_GATEWAY_VERSION','registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway'),
@('model-conversion-worker','MODEL_CONVERSION_WORKER_VERSION','UPDATE_WORKER_VERSION','registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker'),
@('easyai-updater','UPDATER_VERSION','UPDATE_UPDATER_VERSION','registry.cn-shanghai.aliyuncs.com/easyaigc/easyai-updater'),
@('easyai-asg','ASG_VERSION','UPDATE_ASG_VERSION','registry.cn-shanghai.aliyuncs.com/easyaigc/agent-governance'),
@('agent-memory','AMS_VERSION','UPDATE_AMS_VERSION','registry.cn-shanghai.aliyuncs.com/easyaigc/agent-memory')
)
$plan = @()
foreach ($item in $mapping) {
$service,$key,$option,$image = $item
$value = [Environment]::GetEnvironmentVariable($option)
$isSelected = $selected.Count -eq 0 -or $service -in $selected
if ($value -and -not $isSelected) { throw "$option 要求在 UPDATE_SERVICES 中指定 $service" }
if (-not $isSelected) { continue }
if (-not $value) { $value = $env:UPDATE_RELEASE_VERSION }
if (-not $value) { $value = Get-EnvValue $content $key }
if (-not $value) { $value = Get-EnvValue $content 'VERSION' }
$pattern = '^\d+\.\d+\.\d+\.\d{8}(\.\d+)?$'
if ($service -eq 'easyai-web') { $pattern = '^\d+\.\d+\.\d+\.\d{8}(\.\d+)?(-no-ssr|-hmg-ai|-easyai|-chinacuc)?$' }
if (-not $value -or $value -notmatch $pattern) { throw "$service 缺少固定版本;请指定 $option / UPDATE_RELEASE_VERSION,latest/dev 等浮动标签不能用于应用更新" }
$plan += [PSCustomObject]@{ Service=$service; Key=$key; Version=$value; Image=$image }
}
if ($env:UPDATE_RELEASE_VERSION -and ($selected.Count -eq 0 -or ('easyai-server' -in $selected -and 'easyai-web' -in $selected))) {
$oldDefault = Get-EnvValue $content 'VERSION'
foreach ($item in $mapping) {
$service,$key,$option,$image = $item
if ($selected.Count -gt 0 -and $service -notin $selected -and -not (Get-EnvValue $content $key) -and $oldDefault) {
$plan += [PSCustomObject]@{ Service=''; Key=$key; Version=$oldDefault; Image='' }
}
}
$plan += [PSCustomObject]@{ Service=''; Key='VERSION'; Version=$env:UPDATE_RELEASE_VERSION; Image='' }
}
return $plan
}
function Set-ApplicationVersions {
param([string]$Path, [object[]]$Plan)
$content = Get-Content $Path -Raw -Encoding UTF8
foreach ($item in $Plan) { $content = Set-EnvValue $content $item.Key $item.Version }
[System.IO.File]::WriteAllText($Path, $content, [System.Text.UTF8Encoding]::new($false))
foreach ($item in $Plan) { Write-Host "📌 已持久化应用版本: $($item.Key)=$($item.Version)" }
}
function Test-ApplicationVersionImages {
param([object[]]$Plan, [string[]]$ComposeCommand)
foreach ($item in $Plan) {
if (-not $item.Service) { continue }
$args = @($ComposeCommand | Select-Object -Skip 1) + @('config','--format','json',$item.Service)
$executable = $ComposeCommand[0]
$configuration = & $executable @args
if ($LASTEXITCODE -ne 0) { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' }
try {
# Compose includes dependencies even when a service is selected.
# Keep the configuration in memory; do not log its environment.
$resolved = ($configuration -join "`n") | ConvertFrom-Json -ErrorAction Stop
$actual = $resolved.services.PSObject.Properties[$item.Service].Value.image
} catch { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' }
if ("$actual".Trim() -ne "$($item.Image):$($item.Version)") {
throw "$($item.Service) 的有效镜像未使用环境中的固定版本;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务"
}
}
}
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# 仅规划/持久化已选应用的固定版本;由 update.sh 调用,不编辑 Compose。
APPLICATION_VERSION_MAP='easyai-server SERVER_VERSION UPDATE_SERVER_VERSION registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server
easyai-web WEB_VERSION UPDATE_WEB_VERSION registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai
ws-gateway WS_GATEWAY_VERSION UPDATE_GATEWAY_VERSION registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway
model-conversion-worker MODEL_CONVERSION_WORKER_VERSION UPDATE_WORKER_VERSION registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker
easyai-updater UPDATER_VERSION UPDATE_UPDATER_VERSION registry.cn-shanghai.aliyuncs.com/easyaigc/easyai-updater
easyai-asg ASG_VERSION UPDATE_ASG_VERSION registry.cn-shanghai.aliyuncs.com/easyaigc/agent-governance
agent-memory AMS_VERSION UPDATE_AMS_VERSION registry.cn-shanghai.aliyuncs.com/easyaigc/agent-memory'
application_selected() {
[ -z "${UPDATE_SERVICES:-}" ] || [[ " ${UPDATE_SERVICES} " == *" $1 "* ]]
}
plan_application_versions() {
local file="$1" service key option image value suffix base old_default
APPLICATION_VERSION_WRITES=()
while read -r service key option image; do
value="${!option:-}"
if [ -n "$value" ] && ! application_selected "$service"; then
echo "❌ $option 要求在 UPDATE_SERVICES 中指定 $service;尚未修改环境或容器" >&2
return 1
fi
application_selected "$service" || continue
if [ -z "$value" ] && [ -n "${UPDATE_RELEASE_VERSION:-}" ]; then value="$UPDATE_RELEASE_VERSION"; fi
if [ -z "$value" ]; then value="$(read_env_value "$file" "$key")"; fi
if [ -z "$value" ]; then value="$(read_env_value "$file" VERSION)"; fi
suffix=''
if [ "$service" = easyai-web ]; then suffix='(-no-ssr|-hmg-ai|-easyai|-chinacuc)?'; fi
if ! [[ "$value" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]{8}(\.[0-9]+)?${suffix}$ ]]; then
echo "❌ $service 缺少固定版本:请指定 $option 或 UPDATE_RELEASE_VERSION,或在 .env 保留有效 $key;latest/dev 等浮动标签不能用于应用更新" >&2
return 1
fi
APPLICATION_VERSION_WRITES+=("$key=$value")
done <<< "$APPLICATION_VERSION_MAP"
if [ -n "${UPDATE_RELEASE_VERSION:-}" ] && application_selected easyai-server && application_selected easyai-web; then
# VERSION 仍保留兼容入口;改变它前先冻结未选中服务的旧继承值。
old_default="$(read_env_value "$file" VERSION)"
while read -r service key option image; do
application_selected "$service" && continue
value="$(read_env_value "$file" "$key")"
if [ -z "$value" ] && [ -n "$old_default" ]; then APPLICATION_VERSION_WRITES+=("$key=$old_default"); fi
done <<< "$APPLICATION_VERSION_MAP"
APPLICATION_VERSION_WRITES+=("VERSION=$UPDATE_RELEASE_VERSION")
fi
}
apply_application_versions() {
local file="$1" entry temporary
temporary="$(mktemp "${file}.versions.XXXXXX")"
cp "$file" "$temporary"
chmod 600 "$temporary"
for entry in "${APPLICATION_VERSION_WRITES[@]}"; do
write_env_value "$temporary" "${entry%%=*}" "${entry#*=}"
done
mv "$temporary" "$file"
for entry in "${APPLICATION_VERSION_WRITES[@]}"; do echo "📌 已持久化应用版本: $entry"; done
}
verify_application_version_images() {
local file="$1" compose="$2" service key option image version actual configuration
while read -r service key option image; do
application_selected "$service" || continue
version="$(read_env_value "$file" "$key")"
# --images SERVICE includes dependency images on Compose 5. Read the
# selected service from normalized config instead of comparing the list.
configuration="$(sudo $compose config --format json "$service")" || return 1
actual="$(printf '%s' "$configuration" | python3 -c '
import json, sys
try:
image = json.load(sys.stdin)["services"][sys.argv[1]]["image"]
if not isinstance(image, str): raise ValueError()
print(image)
except (ValueError, KeyError, TypeError):
raise SystemExit("无法读取应用的有效镜像定义;尚未拉取或重建服务")
' "$service")" || return 1
if [ "$actual" != "$image:$version" ]; then
echo "❌ $service 的有效镜像未使用 .env 中的固定版本 $key=$version;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" >&2
return 1
fi
done <<< "$APPLICATION_VERSION_MAP"
}
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { readFileSync, writeFileSync, renameSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const VERSION = /^\d+\.\d+\.\d+\.\d{8}(?:\.\d+)?$/;
const TARGETS = {
'hmg-ai': ['registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai','latest-hmg-ai','-hmg-ai'],
easyai: ['registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai','latest-easyai','-easyai'],
chinacuc: ['registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai','latest-chinacuc','-chinacuc'],
'server-main': ['registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server','latest',''],
web: ['registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai','latest',''],
'web-no-ssr': ['registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai','latest-no-ssr','-no-ssr'],
'ws-gateway': ['registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway','latest',''],
'model-conversion-worker': ['registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker','latest',''],
updater: ['registry.cn-shanghai.aliyuncs.com/easyaigc/easyai-updater','latest',''],
'service-governance': ['registry.cn-shanghai.aliyuncs.com/easyaigc/agent-governance','latest',''],
'memory-service': ['registry.cn-shanghai.aliyuncs.com/easyaigc/agent-memory','latest',''],
};
const CORE = ['server-main','web','ws-gateway'];
const docker = args => execFileSync('docker', args, {encoding:'utf8', stdio:['ignore','pipe','pipe']});
const inspectArgs = ref => ['buildx','imagetools','inspect',ref,'--format','{{json .Manifest}}'];
export function promotionPlan(manifest, version, confirmedVersion) {
if (!VERSION.test(version ?? '') || version !== confirmedVersion) throw new Error('必须人工指定完整 --version 并用相同 --confirm-version 确认,不能自动选择最新版');
if (manifest.schemaVersion !== 1 || manifest.channel !== 'versioned' || manifest.productVersion !== version || manifest.platform !== 'linux/amd64') throw new Error('候选版本记录与指定版本/平台不一致');
if (typeof manifest.releaseId !== 'string' || !manifest.releaseId.trim() || manifest.releaseId.length > 200 || !Array.isArray(manifest.components)) throw new Error('候选版本记录缺少有效 releaseId/components');
if (!Number.isFinite(Date.parse(manifest.publishedAt)) || ['managedUpdateAllowed','scheduledUpdateAllowed','rollbackSafe'].some(key=>typeof manifest[key]!=='boolean')) throw new Error('候选版本记录缺少有效发布时间或更新兼容性设置');
const seen = new Set();
const images = manifest.components.map(component => {
const target = TARGETS[component.key];
if (!target || seen.has(component.key) || component.image !== target[0] || !/^sha256:[a-f0-9]{64}$/.test(component.digest ?? '')) throw new Error(`无效或重复组件: ${component.key}`);
seen.add(component.key);
const tag = component.tag ?? `${version}${target[2]}`;
const base = target[2] && tag.endsWith(target[2]) ? tag.slice(0,-target[2].length) : tag;
if (!VERSION.test(base) || (target[2] && !tag.endsWith(target[2]))) throw new Error(`组件 ${component.key} 必须引用固定版本标签`);
return {key:component.key, image:component.image, digest:component.digest, source:`${component.image}:${tag}`, stable:`${component.image}:${target[1]}`};
});
for (const key of [...CORE,'web-no-ssr']) if (!seen.has(key)) throw new Error(`候选记录缺少必要组件: ${key}`);
return images;
}
export function promoteStableRelease({manifest, version, confirmedVersion, outputPath, dryRun=false, run=docker}) {
const plan = promotionPlan(manifest, version, confirmedVersion);
// 所有固定标签先核验完成,再发生任何稳定标签写入。
for (const image of plan) {
const descriptor = JSON.parse(run(inspectArgs(image.source)));
let platforms = descriptor.manifests?.map(item=>item.platform) ?? [descriptor.platform];
if (!platforms.some(Boolean)) {
const verbose = JSON.parse(run(['manifest','inspect','--verbose',image.source]));
platforms = (Array.isArray(verbose) ? verbose : [verbose]).map(item=>item.Descriptor?.platform);
}
if (descriptor.digest !== image.digest || !platforms.some(platform=>platform?.os==='linux' && platform?.architecture==='amd64')) throw new Error(`固定产物核验失败: ${image.key};稳定标签未修改`);
}
if (dryRun) return {status:'DRY_RUN',version,images:plan};
if (!outputPath) throw new Error('缺少稳定清单输出路径');
const previous = new Map();
for (const image of plan) {
try { previous.set(image.stable, JSON.parse(run(inspectArgs(image.stable))).digest); }
catch (error) {
if (!/manifest unknown|not found|no such manifest/i.test(String(error.stderr ?? error.message))) throw error;
previous.set(image.stable,null);
}
}
const changed = [];
try {
for (const image of plan) {
// 先登记尝试;写入成功但回执丢失时也必须尝试恢复原稳定指针。
changed.push(image);
run(['buildx','imagetools','create','--prefer-index=false','--tag',image.stable,`${image.image}@${image.digest}`]);
const actual = JSON.parse(run(inspectArgs(image.stable)));
if (actual.digest !== image.digest) throw new Error(`稳定标签读回不一致: ${image.key}`);
}
const promoted = {...manifest, channel:'stable', publishedAt:new Date().toISOString(),
promotedFromPublishedAt:manifest.publishedAt, components:manifest.components.filter(item=>CORE.includes(item.key))};
const temporary = `${outputPath}.tmp`;
writeFileSync(temporary, JSON.stringify(promoted,null,2)+'\n');
renameSync(temporary,outputPath);
return {status:'PROMOTED',version,images:plan,manifest:outputPath};
} catch (error) {
const recovery = [];
for (const image of changed.reverse()) {
const oldDigest = previous.get(image.stable);
if (!oldDigest) { recovery.push(`${image.key}: 原稳定标签不存在,保留本次标签并记录未完成`); continue; }
try {
run(['buildx','imagetools','create','--prefer-index=false','--tag',image.stable,`${image.image}@${oldDigest}`]);
if (JSON.parse(run(inspectArgs(image.stable))).digest !== oldDigest) throw new Error('恢复读回不一致');
recovery.push(`${image.key}: 已恢复原稳定标签`);
} catch { recovery.push(`${image.key}: 恢复失败,需按原 digest 继续恢复`); }
}
throw new Error(`稳定提升未完成,未提交新的稳定清单:${error.message};${recovery.join(';')}`);
}
}
function main() {
const options = {};
const args = process.argv.slice(2);
for (let index=0; index<args.length; index++) {
const key=args[index];
if (key==='--dry-run') options.dryRun=true;
else if (['--version','--confirm-version','--manifest','--output'].includes(key)) {
const value=args[++index];
if (!value || value.startsWith('--')) throw new Error(`${key} 缺少参数`);
options[key.slice(2)]=value;
} else throw new Error(`未知选项: ${key}`);
}
// 在读取候选文件、调用 Docker 前先拒绝未确认或浮动版本。
if (!VERSION.test(options.version ?? '') || options.version!==options['confirm-version']) throw new Error('用法: node scripts/promote-stable-release.mjs --version X.Y.Z.YYYYMMDD[.N] --confirm-version 同一版本 [--dry-run];仅在用户明确指定版本转稳定版后执行');
const manifestPath=resolve(options.manifest ?? `${ROOT}/system-update/releases/${options.version}.json`);
const manifest=JSON.parse(readFileSync(manifestPath,'utf8'));
console.log(JSON.stringify(promoteStableRelease({manifest,version:options.version,confirmedVersion:options['confirm-version'],
outputPath:resolve(options.output ?? `${ROOT}/system-update/release-manifest.json`),dryRun:options.dryRun}),null,2));
}
if (process.argv[1] && import.meta.url===pathToFileURL(resolve(process.argv[1])).href) {
try { main(); } catch (error) { console.error(error.message); process.exitCode=1; }
}
+100
View File
@@ -0,0 +1,100 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promotionPlan, promoteStableRelease } from './promote-stable-release.mjs';
const version='3.29.0.20261014.2';
const manifest={schemaVersion:1,channel:'versioned',productVersion:version,platform:'linux/amd64',releaseId:'release-id',managedUpdateAllowed:true,scheduledUpdateAllowed:true,rollbackSafe:false,publishedAt:'2026-10-14T00:00:00Z',
components:[
['server-main','comfy-ai/comfy-server',''],['web','comfy-ai/one-ai',''],
['web-no-ssr','comfy-ai/one-ai','-no-ssr'],['ws-gateway','easyaigc/wsgateway',''],
['model-conversion-worker','easyaigc/model-conversion-worker',''],
].map(([key,image,suffix],index)=>({key,image:'registry.cn-shanghai.aliyuncs.com/'+image,tag:version+suffix,digest:'sha256:'+String(index+1).repeat(64),buildId:'source-sha'}))};
function fixture() {
const plan=promotionPlan(manifest,version,version);
const tags=new Map(plan.flatMap(image=>[[image.source,image.digest],[image.stable,'sha256:'+'9'.repeat(64)]]));
const calls=[];
const run=args=>{
calls.push(args);
if (args[0]==='manifest') return JSON.stringify({Descriptor:{platform:{os:'linux',architecture:'amd64'}}});
if (args[2]==='inspect') {
const ref=args[3];
// 真实单平台 imagetools 输出没有 platform,需读取 verbose manifest。
return JSON.stringify({digest:tags.get(ref),...(ref.includes('comfy-server') ? {} : {manifests:[{platform:{os:'linux',architecture:'amd64'}}]})});
}
assert.equal(args[2],'create');
assert.ok(args.includes('--prefer-index=false'));
const ref=args[args.indexOf('--tag')+1];
const digest=args.at(-1).split('@')[1];
tags.set(ref,digest);
return '';
};
return {plan,tags,calls,run};
}
test('未明确确认同一个版本及浮动版本都不调用Docker',()=>{
let called=false;
for (const [candidate,confirm] of [[version,undefined],[version,'3.29.0.20261014.3'],['latest','latest']]) {
assert.throws(()=>promoteStableRelease({manifest,version:candidate,confirmedVersion:confirm,run:()=>{called=true;}}),/人工指定/);
}
assert.equal(called,false);
});
test('候选必须包括SSR/noSSR,并拒绝重复、错误镜像和浮动源标签',()=>{
const variant = {...manifest.components[1],key:'hmg-ai',tag:version+'-hmg-ai'};
assert.equal(promotionPlan({...manifest,components:[...manifest.components,variant]},version,version).at(-1).stable,variant.image+':latest-hmg-ai');
for (const components of [manifest.components.filter(item=>item.key!=='web-no-ssr'),
[...manifest.components,manifest.components[0]],
manifest.components.map((item,index)=>index?item:{...item,image:'wrong/image'}),
manifest.components.map((item,index)=>index?item:{...item,tag:'latest'})]) {
assert.throws(()=>promotionPlan({...manifest,components},version,version));
}
});
test('全部固定产物先核验;任一digest不匹配时稳定标签和清单保持',()=>{
const f=fixture();
f.tags.set(f.plan.at(-1).source,'sha256:'+'8'.repeat(64));
assert.throws(()=>promoteStableRelease({manifest,version,confirmedVersion:version,outputPath:'unused',run:f.run}),/核验失败/);
assert.ok(f.calls.every(args=>args[2]!=='create'));
assert.ok(f.plan.every(image=>f.tags.get(image.stable)==='sha256:'+'9'.repeat(64)));
});
test('dry-run核验固定版本且不写任何稳定标签或文件',()=>{
const f=fixture();
assert.equal(promoteStableRelease({manifest,version,confirmedVersion:version,dryRun:true,run:f.run}).status,'DRY_RUN');
assert.ok(f.calls.every(args=>args[2]!=='create'));
});
test('人工提升只按已核验digest补稳定标签,两种前端分别保留;不构建业务或改源版本',()=>{
const dir=mkdtempSync(join(tmpdir(),'easyai-promote-'));
try {
const path=join(dir,'stable.json');
const f=fixture();
const result=promoteStableRelease({manifest,version,confirmedVersion:version,outputPath:path,run:f.run});
assert.equal(result.status,'PROMOTED');
assert.ok(f.plan.every(image=>f.tags.get(image.stable)===image.digest));
assert.ok(f.plan.every(image=>f.tags.get(image.source)===image.digest));
assert.equal(JSON.parse(readFileSync(path)).channel,'stable');
assert.deepEqual(JSON.parse(readFileSync(path)).components.map(item=>item.key),['server-main','web','ws-gateway']);
assert.ok(f.calls.every(args=>args[0]!=='build' && args[0]!=='push'));
} finally { rmSync(dir,{recursive:true,force:true}); }
});
test('稳定别名写入后回执失败时恢复原指针,稳定清单不提前写入',()=>{
const dir=mkdtempSync(join(tmpdir(),'easyai-promote-fail-'));
try {
const path=join(dir,'stable.json'); writeFileSync(path,'old stable manifest');
const f=fixture(); let once=true;
const run=args=>{
const output=f.run(args);
if (args[2]==='create' && args.includes(f.plan[2].stable) && once) { once=false; throw new Error('write succeeded but response lost'); }
return output;
};
assert.throws(()=>promoteStableRelease({manifest,version,confirmedVersion:version,outputPath:path,run}),/未完成.*已恢复/);
assert.ok(f.plan.every(image=>f.tags.get(image.stable)==='sha256:'+'9'.repeat(64)));
assert.equal(readFileSync(path,'utf8'),'old stable manifest');
} finally { rmSync(dir,{recursive:true,force:true}); }
});
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Exercise Bash and PowerShell image verification with real Compose config.
No images are pulled and no containers are created. Fixture settings are fake.
"""
import argparse
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
parser = argparse.ArgumentParser()
parser.add_argument('--source', type=Path, default=Path(__file__).resolve().parent.parent)
args = parser.parse_args()
source = args.source.resolve()
started = time.monotonic()
version = '1.0.0.20261014'
image = 'registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server'
secret = 'synthetic-config-value-must-not-be-printed'
with tempfile.TemporaryDirectory(prefix='easyai-image-config-') as temporary:
root = Path(temporary)
(root / '.env').write_text(f'SERVER_VERSION={version}\nFIXTURE_SECRET={secret}\n')
binaries = root / 'bin'
binaries.mkdir()
sudo = binaries / 'sudo'
sudo.write_text('#!/bin/sh\nexec "$@"\n')
sudo.chmod(0o755)
env = {**os.environ, 'PATH': str(binaries) + os.pathsep + os.environ['PATH'], 'UPDATE_SERVICES': 'easyai-server'}
reports = []
for correct in (True, False):
selected = f'{image}:${{SERVER_VERSION}}' if correct else f'{image}:latest'
# On the negative case a dependency deliberately has the expected image;
# checking membership in --images output would wrongly approve it.
dependency = 'redis:7-alpine' if correct else f'{image}:${{SERVER_VERSION}}'
(root / 'compose.yml').write_text(f'services:\n dependency:\n image: {dependency}\n easyai-server:\n image: {selected}\n depends_on:\n - dependency\n environment:\n FIXTURE_SECRET: ${{FIXTURE_SECRET}}\n')
config = subprocess.run(['docker','compose','config','--format','json','easyai-server'], cwd=root, env=env, text=True, capture_output=True, check=True)
assert json.loads(config.stdout)['services']['easyai-server']['image'] == (f'{image}:{version}' if correct else f'{image}:latest')
images = subprocess.run(['docker','compose','config','--images','easyai-server'], cwd=root, env=env, text=True, capture_output=True, check=True)
assert len(images.stdout.splitlines()) > 1, 'Compose fixture must include dependency images'
bash = ['bash','-c', '. "$1/scripts/init-security-env.sh"; . "$1/scripts/init-application-version-env.sh"; verify_application_version_images .env "docker compose"', 'fixture', str(source)]
powershell = ['pwsh','-NoProfile','-NonInteractive','-Command', f". '{source}/scripts/Initialize-ApplicationVersions.ps1'; Test-ApplicationVersionImages @([pscustomobject]@{{Service='easyai-server';Image='{image}';Version='{version}'}}) @('docker','compose')"]
for name, command in [('bash', bash), ('powershell', powershell)]:
result = subprocess.run(command, cwd=root, env=env, text=True, errors='backslashreplace', capture_output=True, timeout=30)
assert (result.returncode == 0) == correct, f'{name} incorrect verification: {result.stdout} {result.stderr}'
assert secret not in result.stdout + result.stderr, 'configuration leaked'
reports.append({'shell':name, 'correctImage':correct, 'expectedExitObserved':True})
print(json.dumps({'cases':reports,'seconds':round(time.monotonic()-started,3)}))
+33 -5
View File
@@ -37,7 +37,7 @@ def fixture(base):
(root / 'scripts/install-infrastructure-update-task.sh').write_text('#!/bin/sh\nexit 0\n') (root / 'scripts/install-infrastructure-update-task.sh').write_text('#!/bin/sh\nexit 0\n')
(root / 'scripts/Install-InfrastructureUpdateTask.ps1').write_text('# Scheduler transport is verified separately.\n') (root / 'scripts/Install-InfrastructureUpdateTask.ps1').write_text('# Scheduler transport is verified separately.\n')
(root / 'data').mkdir() (root / 'data').mkdir()
(root / '.env').write_text('SYSTEM_UPDATE_MODE=legacy_watchtower\nNUXT_PUBLIC_BASE_APIURL=http://127.0.0.1:3001\nCONFIG_JWT_SECRET=existing-jwt-secret-that-is-long-enough\nWS_AUTH_WS_TICKET_SECRET=existing-ticket-secret-that-is-long-enough\n') (root / '.env').write_text('VERSION=3.23.0.20261003\nMODEL_CONVERSION_WORKER_VERSION=3.23.0.20261003\nSYSTEM_UPDATE_MODE=legacy_watchtower\nNUXT_PUBLIC_BASE_APIURL=http://127.0.0.1:3001\nCONFIG_JWT_SECRET=existing-jwt-secret-that-is-long-enough\nWS_AUTH_WS_TICKET_SECRET=existing-ticket-secret-that-is-long-enough\n')
(root / '.gitignore').write_text('.env\n*.log\n') (root / '.gitignore').write_text('.env\n*.log\n')
commands = base / 'commands.jsonl' commands = base / 'commands.jsonl'
commands.touch() commands.touch()
@@ -57,6 +57,12 @@ if args[:1] == ['inspect']:
sys.exit(0) sys.exit(0)
sys.exit(1) sys.exit(1)
if args[:3] == ['compose', 'config', '--services']: print('easyai-server\\neasyai-web\\nws-gateway\\neasyai-updater\\nmodel-conversion-worker\\nmongo\\nredis') if args[:3] == ['compose', 'config', '--services']: print('easyai-server\\neasyai-web\\nws-gateway\\neasyai-updater\\nmodel-conversion-worker\\nmongo\\nredis')
elif args[:4] == ['compose', 'config', '--format', 'json']:
names = {'easyai-server':('SERVER_VERSION','comfy-ai/comfy-server'), 'easyai-web':('WEB_VERSION','comfy-ai/one-ai'), 'ws-gateway':('WS_GATEWAY_VERSION','easyaigc/wsgateway'), 'easyai-updater':('UPDATER_VERSION','easyaigc/easyai-updater'), 'model-conversion-worker':('MODEL_CONVERSION_WORKER_VERSION','easyaigc/model-conversion-worker')}
values = dict(line.split('=',1) for line in open('.env').read().splitlines() if '=' in line and not line.startswith('#'))
key,image = names[args[-1]]
actual = 'registry.cn-shanghai.aliyuncs.com/'+image+':'+('latest' if os.environ.get('UPDATE_TEST_IMAGE_OVERRIDE') == '1' else values[key])
print(json.dumps({'services': {args[-1]: {'image':actual}, 'dependency': {'image':'redis:7-alpine'}}}))
elif args[:3] == ['compose', 'ps', '-q']: print('container-' + args[-1]) elif args[:3] == ['compose', 'ps', '-q']: print('container-' + args[-1])
sys.exit(0) sys.exit(0)
''') ''')
@@ -105,6 +111,19 @@ with tempfile.TemporaryDirectory(prefix='easyai-conversion-update-') as temporar
assert original == (root / '.env').read_bytes(), 'repeat changes deployment settings' assert original == (root / '.env').read_bytes(), 'repeat changes deployment settings'
log.write_text('') log.write_text('')
original = (root / '.env').read_bytes() original = (root / '.env').read_bytes()
(root / '.env').write_text((root / '.env').read_text().replace('VERSION=3.23.0.20261003', 'VERSION=latest'))
rejected_env = (root / '.env').read_bytes()
result = run(command, root, env)
assert result.returncode != 0 and '固定版本' in result.stdout + result.stderr
assert rejected_env == (root / '.env').read_bytes()
assert not any(c[:2] in [['compose','pull'],['compose','up']] for c in calls(log))
(root / '.env').write_bytes(original)
log.write_text('')
result = run(command, root, {**env, 'UPDATE_TEST_IMAGE_OVERRIDE':'1'})
assert result.returncode != 0 and '有效镜像' in result.stdout + result.stderr
assert not any(c[:2] in [['compose','pull'],['compose','up']] for c in calls(log))
log.write_text('')
original = (root / '.env').read_bytes()
result = run(command, root, {**env, 'UPDATE_RELEASE_VERSION': 'latest;unsafe'}) result = run(command, root, {**env, 'UPDATE_RELEASE_VERSION': 'latest;unsafe'})
assert result.returncode != 0 assert result.returncode != 0
assert original == (root / '.env').read_bytes() assert original == (root / '.env').read_bytes()
@@ -121,7 +140,16 @@ with tempfile.TemporaryDirectory(prefix='easyai-conversion-update-') as temporar
result = run(command, root, {**env, 'UPDATE_TEST_UNHEALTHY': '1', 'UPDATE_HEALTH_TIMEOUT_SECONDS': '1'}) result = run(command, root, {**env, 'UPDATE_TEST_UNHEALTHY': '1', 'UPDATE_HEALTH_TIMEOUT_SECONDS': '1'})
assert result.returncode != 0 assert result.returncode != 0
assert '应用更新成功' not in result.stdout assert '应用更新成功' not in result.stdout
if name == 'bash': if name in ('bash','powershell'):
log.write_text('')
before = dict(line.split('=',1) for line in (root / '.env').read_text().splitlines() if '=' in line and not line.startswith('#'))
result = run(command, root, {**env, 'UPDATE_SERVICES':'easyai-server', 'UPDATE_RELEASE_VERSION':'3.25.0.20261009.2'})
assert result.returncode == 0, result.stdout + result.stderr
after = dict(line.split('=',1) for line in (root / '.env').read_text().splitlines() if '=' in line and not line.startswith('#'))
assert after['SERVER_VERSION'] == '3.25.0.20261009.2'
assert after['VERSION'] == before['VERSION'] and after['WEB_VERSION'] == before['WEB_VERSION']
assert after['MODEL_CONVERSION_WORKER_VERSION'] == before['MODEL_CONVERSION_WORKER_VERSION']
assert ['compose','up','-d','--no-deps','easyai-server'] in calls(log)
log.write_text('') log.write_text('')
result = run(command, root, {**env, 'UPDATE_SERVICES': 'easyai-server easyai-web', 'UPDATE_RELEASE_VERSION': '3.25.0.20261009.3'}) result = run(command, root, {**env, 'UPDATE_SERVICES': 'easyai-server easyai-web', 'UPDATE_RELEASE_VERSION': '3.25.0.20261009.3'})
assert result.returncode == 0, result.stdout + result.stderr assert result.returncode == 0, result.stdout + result.stderr
@@ -132,7 +160,7 @@ with tempfile.TemporaryDirectory(prefix='easyai-conversion-update-') as temporar
assert ['compose', 'up', '-d', '--no-deps', 'easyai-server', 'easyai-web'] in calls(log) assert ['compose', 'up', '-d', '--no-deps', 'easyai-server', 'easyai-web'] in calls(log)
assert not any('model-conversion-worker' in call for call in calls(log) if call[:2] in [['compose', 'pull'], ['compose', 'up']]) assert not any('model-conversion-worker' in call for call in calls(log) if call[:2] in [['compose', 'pull'], ['compose', 'up']])
values = core_values values = core_values
print('bash: core-only release preserves non-target Worker pin passed') print(f'{name}: core-only release preserves non-target Worker pin passed')
companion_env = {**env, 'UPDATE_SERVICES': 'ws-gateway easyai-updater', companion_env = {**env, 'UPDATE_SERVICES': 'ws-gateway easyai-updater',
'UPDATE_GATEWAY_VERSION': '3.25.0.20261009', 'UPDATE_GATEWAY_VERSION': '3.25.0.20261009',
'UPDATE_UPDATER_VERSION': '3.25.0.20261009'} 'UPDATE_UPDATER_VERSION': '3.25.0.20261009'}
@@ -154,7 +182,7 @@ with tempfile.TemporaryDirectory(prefix='easyai-conversion-update-') as temporar
assert new_values['VERSION'] == values['VERSION'] assert new_values['VERSION'] == values['VERSION']
assert new_values['MODEL_CONVERSION_WORKER_VERSION'] == values['MODEL_CONVERSION_WORKER_VERSION'] assert new_values['MODEL_CONVERSION_WORKER_VERSION'] == values['MODEL_CONVERSION_WORKER_VERSION']
assert new_values['CONFIG_JWT_SECRET'] == values['CONFIG_JWT_SECRET'] assert new_values['CONFIG_JWT_SECRET'] == values['CONFIG_JWT_SECRET']
print('bash: independent companion pins preserve non-target versions and reject invalid scope passed') print(f'{name}: independent companion pins preserve non-target versions and reject invalid scope passed')
print(f'{name}: selected services, stable secret, invalid service and unhealthy Worker passed') print(f'{name}: selected services, stable secret, invalid service and unhealthy Worker passed')
case = base / 'self-update' case = base / 'self-update'
@@ -178,7 +206,7 @@ with tempfile.TemporaryDirectory(prefix='easyai-conversion-update-') as temporar
for args in [['git', 'add', 'update.sh'], ['git', 'commit', '-qm', 'test: 验证更新脚本自更新'], ['git', 'push', '-q', 'origin', 'HEAD']]: for args in [['git', 'add', 'update.sh'], ['git', 'commit', '-qm', 'test: 验证更新脚本自更新'], ['git', 'push', '-q', 'origin', 'HEAD']]:
assert run(args, seed).returncode == 0 assert run(args, seed).returncode == 0
compose_path = root / 'docker-compose.yml' compose_path = root / 'docker-compose.yml'
compose_path.write_text(compose_path.read_text().replace('comfy-ai/one-ai:${VERSION}', 'comfy-ai/one-ai@sha256:' + 'a' * 64)) compose_path.write_text(compose_path.read_text().replace('comfy-ai/one-ai:${WEB_VERSION:-${VERSION}}', 'comfy-ai/one-ai@sha256:' + 'a' * 64))
result = run(['bash', 'update.sh'], root, {**env, 'UPDATE_MODE': 'full', 'UPDATE_ADOPT_LEGACY_CONFIG': '1', 'UPDATE_RESTORE_REPOSITORY_IMAGES': 'easyai-web'}) result = run(['bash', 'update.sh'], root, {**env, 'UPDATE_MODE': 'full', 'UPDATE_ADOPT_LEGACY_CONFIG': '1', 'UPDATE_RESTORE_REPOSITORY_IMAGES': 'easyai-web'})
assert result.returncode == 0, result.stdout + result.stderr assert result.returncode == 0, result.stdout + result.stderr
assert '切换到新版更新脚本' in result.stdout assert '切换到新版更新脚本' in result.stdout
+2 -2
View File
@@ -27,7 +27,7 @@ grep -qx 'WATCHTOWER_CLEANUP=' "$LEGACY_ENV_FILE"
grep -qx 'WATCHTOWER_HTTP_API_PERIODIC_POLLS=false' "$LEGACY_ENV_FILE" grep -qx 'WATCHTOWER_HTTP_API_PERIODIC_POLLS=false' "$LEGACY_ENV_FILE"
grep -qx 'WATCHTOWER_UPDATE_TARGET=easyai-server easyai-wsgateway easyai-web' "$LEGACY_ENV_FILE" grep -qx 'WATCHTOWER_UPDATE_TARGET=easyai-server easyai-wsgateway easyai-web' "$LEGACY_ENV_FILE"
grep -qx 'WATCHTOWER_IMAGE=registry.cn-shanghai.aliyuncs.com/comfy-ai/watchtower-aliyun:latest' "$LEGACY_ENV_FILE" grep -qx 'WATCHTOWER_IMAGE=registry.cn-shanghai.aliyuncs.com/comfy-ai/watchtower-aliyun:latest' "$LEGACY_ENV_FILE"
grep -qx 'MODEL_CONVERSION_WORKER_VERSION=latest' "$LEGACY_ENV_FILE" ! grep -q '^MODEL_CONVERSION_WORKER_VERSION=' "$LEGACY_ENV_FILE"
grep -qx 'MODEL_CONVERSION_HUNYUAN_ENABLED=false' "$LEGACY_ENV_FILE" grep -qx 'MODEL_CONVERSION_HUNYUAN_ENABLED=false' "$LEGACY_ENV_FILE"
cp "$LEGACY_ENV_FILE" "$TMP_DIR/legacy-first-run.env" cp "$LEGACY_ENV_FILE" "$TMP_DIR/legacy-first-run.env"
bash "$PROJECT_ROOT/scripts/apply-update-env-migrations.sh" "$LEGACY_ENV_FILE" "$PROJECT_ROOT" >/dev/null bash "$PROJECT_ROOT/scripts/apply-update-env-migrations.sh" "$LEGACY_ENV_FILE" "$PROJECT_ROOT" >/dev/null
@@ -57,7 +57,7 @@ read_value() {
[ "$(read_value UPDATE_HEALTH_TIMEOUT_MS)" = "300000" ] [ "$(read_value UPDATE_HEALTH_TIMEOUT_MS)" = "300000" ]
[ "$(read_value DOCKER_CONFIG_FILE)" = "/root/.docker/config.json" ] [ "$(read_value DOCKER_CONFIG_FILE)" = "/root/.docker/config.json" ]
[ "$(read_value EASYAI_DEPLOYMENT_SCHEMA_VERSION)" = "2" ] [ "$(read_value EASYAI_DEPLOYMENT_SCHEMA_VERSION)" = "2" ]
[ "$(read_value MODEL_CONVERSION_WORKER_VERSION)" = "latest" ] [ -z "$(read_value MODEL_CONVERSION_WORKER_VERSION)" ]
[ "$(read_value MODEL_CONVERSION_HUNYUAN_ENABLED)" = "false" ] [ "$(read_value MODEL_CONVERSION_HUNYUAN_ENABLED)" = "false" ]
cp "$ENV_FILE" "$TMP_DIR/first-run.env" cp "$ENV_FILE" "$TMP_DIR/first-run.env"
@@ -1,3 +1,3 @@
# 独立模型转换;不改变现有在线更新模式与付费选择。 # 独立模型转换;不改变现有在线更新模式与付费选择。
MODEL_CONVERSION_WORKER_VERSION=latest
MODEL_CONVERSION_HUNYUAN_ENABLED=false MODEL_CONVERSION_HUNYUAN_ENABLED=false
+10 -10
View File
@@ -1,13 +1,13 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"channel": "stable", "channel": "stable",
"releaseId": "3.28.2.20261011.4-86e94f19e538", "releaseId": "3.29.0.20261014.2-610c1e33ae02",
"productVersion": "3.28.2.20261011.4", "productVersion": "3.29.0.20261014.2",
"platform": "linux/amd64", "platform": "linux/amd64",
"publishedAt": "2026-10-11T09:46:39.697960+00:00", "publishedAt": "2026-10-13T16:59:32.229224+00:00",
"managedUpdateAllowed": true, "managedUpdateAllowed": true,
"scheduledUpdateAllowed": true, "scheduledUpdateAllowed": true,
"rollbackSafe": true, "rollbackSafe": false,
"minimumUpdaterVersion": "1.1.0", "minimumUpdaterVersion": "1.1.0",
"minimumDeploymentSchemaVersion": 2, "minimumDeploymentSchemaVersion": 2,
"requiredFreeBytes": 8589934592, "requiredFreeBytes": 8589934592,
@@ -15,9 +15,9 @@
{ {
"key": "server-main", "key": "server-main",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server", "image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server",
"digest": "sha256:2077104a3d9f1cba68eb31911bb00a26d2c154eda2c897fb031c162559379e11", "digest": "sha256:a146cd64452f830ba589497be77edce10014a64de6d61abeb32533652dfc7cdc",
"buildId": "607c5ee243936bba394579c7bc94ca0b2f62cda6", "buildId": "b7d82b76bd49c02aacad50bfe778dc256a41c4c0",
"imageSizeBytes": 989020204 "imageSizeBytes": 990412653
}, },
{ {
"key": "ws-gateway", "key": "ws-gateway",
@@ -29,9 +29,9 @@
{ {
"key": "web", "key": "web",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai", "image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"digest": "sha256:279da715ec67c2017c06ca31c8f7e172b703b99c5378404d24198af919c460dc", "digest": "sha256:0140956e880d0c9eec0da1ebd1e3bcba20312d96b0161b5978c8c13f16335e5b",
"buildId": "f1925755cb481b9c3a609996333d26bf47da5302", "buildId": "c685301b169bdcd662a97769a33bf69e3668f4c3",
"imageSizeBytes": 347958996 "imageSizeBytes": 348004004
} }
] ]
} }
@@ -0,0 +1,56 @@
{
"schemaVersion": 1,
"channel": "versioned",
"releaseId": "3.29.0.20261014.2-610c1e33ae02",
"productVersion": "3.29.0.20261014.2",
"platform": "linux/amd64",
"publishedAt": "2026-10-13T16:59:32.229224+00:00",
"managedUpdateAllowed": true,
"scheduledUpdateAllowed": true,
"rollbackSafe": false,
"minimumUpdaterVersion": "1.1.0",
"minimumDeploymentSchemaVersion": 2,
"requiredFreeBytes": 8589934592,
"components": [
{
"key": "server-main",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server",
"digest": "sha256:a146cd64452f830ba589497be77edce10014a64de6d61abeb32533652dfc7cdc",
"buildId": "b7d82b76bd49c02aacad50bfe778dc256a41c4c0",
"imageSizeBytes": 990412653,
"tag": "3.29.0.20261014.2"
},
{
"key": "ws-gateway",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway",
"digest": "sha256:1dd4247cf6a8a21d336876b5d97ecdc3e4ce43199b2f31aff18e0fab47bb4dca",
"buildId": "retained-formal-image",
"imageSizeBytes": 751971874,
"tag": "3.25.0.20261009"
},
{
"key": "web",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"digest": "sha256:0140956e880d0c9eec0da1ebd1e3bcba20312d96b0161b5978c8c13f16335e5b",
"buildId": "c685301b169bdcd662a97769a33bf69e3668f4c3",
"imageSizeBytes": 348004004,
"tag": "3.29.0.20261014.2"
},
{
"key": "web-no-ssr",
"image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai",
"tag": "3.29.0.20261014.2-no-ssr",
"digest": "sha256:cbc47061109e21a4b9d668dc3cefe3fda8cb3058124ebab333dafd087a4aa49b",
"buildId": "c685301b169bdcd662a97769a33bf69e3668f4c3",
"imageSizeBytes": 312754106
},
{
"key": "model-conversion-worker",
"image": "registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker",
"tag": "3.25.0.20261009.2",
"digest": "sha256:c281a3ad7ae2ae9a2e47411fb565e5fa1c35a756c09d1e281d01803ed090932e",
"buildId": "retained-formal-image"
}
],
"recordSource": "既有3.29.0.20261014.2成功生产/文档记录;仅建立版本记录,未执行新的稳定提升"
}
+9 -9
View File
@@ -59,7 +59,10 @@ if ($args -contains "-h" -or $args -contains "--help") {
Write-Host " UPDATE_SERVICES='easyai-server easyai-web model-conversion-worker'" Write-Host " UPDATE_SERVICES='easyai-server easyai-web model-conversion-worker'"
Write-Host " 仅拉取并重建指定服务,不重建其依赖;留空保持整组更新。" Write-Host " 仅拉取并重建指定服务,不重建其依赖;留空保持整组更新。"
Write-Host " UPDATE_RELEASE_VERSION=固定版本" Write-Host " UPDATE_RELEASE_VERSION=固定版本"
Write-Host " 通过脚本固定核心服务和转换 Worker 的镜像版本,不提升 latest。" Write-Host " 将已选应用的固定版本持久化到 .env;latest 仅为人工稳定版。"
Write-Host " UPDATE_SERVER_VERSION / UPDATE_WEB_VERSION / UPDATE_WORKER_VERSION=固定版本"
Write-Host " UPDATE_GATEWAY_VERSION / UPDATE_UPDATER_VERSION / UPDATE_ASG_VERSION / UPDATE_AMS_VERSION=固定版本"
Write-Host " 独立参数优先,只改变选中应用的版本。"
Write-Host " UPDATE_NO_WAIT=1" Write-Host " UPDATE_NO_WAIT=1"
Write-Host " 结束或失败时不等待按 Enter" Write-Host " 结束或失败时不等待按 Enter"
Write-Host " UPDATE_HEALTH_TIMEOUT_SECONDS=秒数" Write-Host " UPDATE_HEALTH_TIMEOUT_SECONDS=秒数"
@@ -201,6 +204,8 @@ if (-not (Test-Path ".env")) { Write-Err "未找到 .env,请先执行 start.ps
# 每次更新都执行版本化、幂等的环境配置迁移;已有非空自定义值保持不变。 # 每次更新都执行版本化、幂等的环境配置迁移;已有非空自定义值保持不变。
. (Join-Path $scriptDir "scripts\Initialize-SecurityEnv.ps1") . (Join-Path $scriptDir "scripts\Initialize-SecurityEnv.ps1")
$environmentPath = Join-Path $scriptDir ".env" $environmentPath = Join-Path $scriptDir ".env"
. (Join-Path $scriptDir "scripts/Initialize-ApplicationVersions.ps1")
try { $applicationVersionPlan = @(Get-ApplicationVersionPlan -Path $environmentPath) } catch { Write-Err $_.Exception.Message }
$environmentContent = Get-Content $environmentPath -Raw -Encoding UTF8 $environmentContent = Get-Content $environmentPath -Raw -Encoding UTF8
if (-not (Get-EnvValue $environmentContent "SYSTEM_UPDATE_MODE")) { if (-not (Get-EnvValue $environmentContent "SYSTEM_UPDATE_MODE")) {
& docker inspect easyai-server *> $null & docker inspect easyai-server *> $null
@@ -222,14 +227,7 @@ Initialize-ServerHttpBindIp -Path (Join-Path $scriptDir ".env")
. (Join-Path $scriptDir "scripts\Initialize-SecurityEnv.ps1") . (Join-Path $scriptDir "scripts\Initialize-SecurityEnv.ps1")
Initialize-SecurityEnv -Path (Join-Path $scriptDir ".env") -Mode "upgrade" Initialize-SecurityEnv -Path (Join-Path $scriptDir ".env") -Mode "upgrade"
if ($env:UPDATE_RELEASE_VERSION) { Set-ApplicationVersions -Path $environmentPath -Plan $applicationVersionPlan
$environmentPath = Join-Path $scriptDir ".env"
$environmentContent = Get-Content -Raw -Encoding UTF8 $environmentPath
$environmentContent = Set-EnvValue $environmentContent "VERSION" $env:UPDATE_RELEASE_VERSION
$environmentContent = Set-EnvValue $environmentContent "MODEL_CONVERSION_WORKER_VERSION" $env:UPDATE_RELEASE_VERSION
[System.IO.File]::WriteAllText($environmentPath, $environmentContent, (New-Object System.Text.UTF8Encoding $false))
Write-Step "📌 已固定本次发布版本: $($env:UPDATE_RELEASE_VERSION)"
}
# 在更新镜像和重建服务前迁移并校验后端公开 API 地址。 # 在更新镜像和重建服务前迁移并校验后端公开 API 地址。
. (Join-Path $scriptDir "scripts\Initialize-PublicApiBaseUrl.ps1") . (Join-Path $scriptDir "scripts\Initialize-PublicApiBaseUrl.ps1")
@@ -372,6 +370,8 @@ if (-not [string]::IsNullOrWhiteSpace($env:UPDATE_SERVICES)) {
if ($availableServices -notcontains $service) { Write-Err "更新服务不存在: $service;尚未拉取或重建任何服务" } if ($availableServices -notcontains $service) { Write-Err "更新服务不存在: $service;尚未拉取或重建任何服务" }
} }
} }
$composeCommand = if ($hasComposeV2) { @("docker","compose") } else { @("docker-compose") }
try { Test-ApplicationVersionImages -Plan $applicationVersionPlan -ComposeCommand $composeCommand } catch { Write-Err $_.Exception.Message }
$upArguments = @("up", "-d") $upArguments = @("up", "-d")
if ($updateServices.Count -gt 0) { $upArguments += @("--no-deps") + $updateServices } if ($updateServices.Count -gt 0) { $upArguments += @("--no-deps") + $updateServices }
if ($updateServices.Count -eq 0 -or $updateServices -contains "redis") { if ($updateServices.Count -eq 0 -or $updateServices -contains "redis") {
+9 -16
View File
@@ -20,9 +20,12 @@ if [[ "${1:-}" =~ ^(-h|--help)$ ]]; then
echo " UPDATE_SERVICES='easyai-server easyai-web model-conversion-worker'" echo " UPDATE_SERVICES='easyai-server easyai-web model-conversion-worker'"
echo " 仅拉取并重建指定服务;不重建其依赖。留空保持整组更新。" echo " 仅拉取并重建指定服务;不重建其依赖。留空保持整组更新。"
echo " UPDATE_RELEASE_VERSION=固定版本" echo " UPDATE_RELEASE_VERSION=固定版本"
echo " 通过脚本固定核心服务和转换 Worker 的镜像版本,不提升 latest。" echo " 通过脚本将已选应用的固定版本持久化到 .env;latest 仅为人工稳定版。"
echo " UPDATE_GATEWAY_VERSION=固定版本 / UPDATE_UPDATER_VERSION=固定版本" echo " UPDATE_GATEWAY_VERSION=固定版本 / UPDATE_UPDATER_VERSION=固定版本"
echo " 仅对已选中的网关或更新服务固定已核验镜像。" echo " 仅对已选中的网关或更新服务固定已核验镜像。"
echo " UPDATE_SERVER_VERSION / UPDATE_WEB_VERSION / UPDATE_WORKER_VERSION=固定版本"
echo " 单独指定组件版本;不改变未选中服务的版本。"
echo " UPDATE_ASG_VERSION / UPDATE_AMS_VERSION=固定版本"
echo " UPDATE_ADOPT_LEGACY_CONFIG=1" echo " UPDATE_ADOPT_LEGACY_CONFIG=1"
echo " 备份旧部署配置并将本地差异迁移为独立覆盖文件。" echo " 备份旧部署配置并将本地差异迁移为独立覆盖文件。"
echo " UPDATE_RESTORE_REPOSITORY_IMAGES='easyai-web'" echo " UPDATE_RESTORE_REPOSITORY_IMAGES='easyai-web'"
@@ -185,6 +188,8 @@ fi
# 已安装但没有 updater 的旧部署保留其更新方式;新安装仍默认 managed。 # 已安装但没有 updater 的旧部署保留其更新方式;新安装仍默认 managed。
# shellcheck source=scripts/init-security-env.sh # shellcheck source=scripts/init-security-env.sh
. ./scripts/init-security-env.sh . ./scripts/init-security-env.sh
. ./scripts/init-application-version-env.sh
plan_application_versions .env
if [ -z "$(read_env_value .env SYSTEM_UPDATE_MODE)" ] && docker inspect easyai-server >/dev/null 2>&1 && ! docker inspect easyai-updater >/dev/null 2>&1; then if [ -z "$(read_env_value .env SYSTEM_UPDATE_MODE)" ] && docker inspect easyai-server >/dev/null 2>&1 && ! docker inspect easyai-updater >/dev/null 2>&1; then
write_env_value .env SYSTEM_UPDATE_MODE legacy_watchtower write_env_value .env SYSTEM_UPDATE_MODE legacy_watchtower
fi fi
@@ -200,21 +205,7 @@ init_server_http_bind_ip .env
init_security_env .env upgrade init_security_env .env upgrade
bash ./scripts/init-infrastructure-update-env.sh "$SCRIPT_DIR" bash ./scripts/init-infrastructure-update-env.sh "$SCRIPT_DIR"
if [ -n "${UPDATE_RELEASE_VERSION:-}" ]; then apply_application_versions .env
write_env_value .env VERSION "$UPDATE_RELEASE_VERSION"
# 定向发布主服务时保留未参与的 Worker 固定版本。
if [ -z "${UPDATE_SERVICES:-}" ] || [[ " ${UPDATE_SERVICES} " == *" model-conversion-worker "* ]]; then
write_env_value .env MODEL_CONVERSION_WORKER_VERSION "$UPDATE_RELEASE_VERSION"
fi
echo "📌 已固定本次发布版本: $UPDATE_RELEASE_VERSION"
fi
if [ -n "${UPDATE_GATEWAY_VERSION:-}" ]; then
write_env_value .env WS_GATEWAY_VERSION "$UPDATE_GATEWAY_VERSION"
fi
if [ -n "${UPDATE_UPDATER_VERSION:-}" ]; then
write_env_value .env UPDATER_VERSION "$UPDATE_UPDATER_VERSION"
fi
# 在更新镜像和重建服务前迁移并校验后端公开 API 地址。 # 在更新镜像和重建服务前迁移并校验后端公开 API 地址。
# shellcheck source=scripts/init-public-api-base-url.sh # shellcheck source=scripts/init-public-api-base-url.sh
@@ -342,6 +333,8 @@ if [ -n "${UPDATE_SERVICES:-}" ]; then
done done
fi fi
verify_application_version_images .env "$DOCKER_COMPOSE_CMD"
# 只检查本次会重建的 Redis;--no-deps 更新其他服务不会修改其旧数据位置。 # 只检查本次会重建的 Redis;--no-deps 更新其他服务不会修改其旧数据位置。
if [ "${#UPDATE_SERVICE_LIST[@]}" -eq 0 ] || printf '%s\n' "${UPDATE_SERVICE_LIST[@]}" | grep -qx 'redis'; then if [ "${#UPDATE_SERVICE_LIST[@]}" -eq 0 ] || printf '%s\n' "${UPDATE_SERVICE_LIST[@]}" | grep -qx 'redis'; then
sudo ./scripts/check-redis-persistence.sh sudo ./scripts/check-redis-persistence.sh