From a00ee10b374649577dae12e7938ca7cc3f141a95 Mon Sep 17 00:00:00 2001 From: wangbo Date: Sun, 27 Sep 2026 11:16:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E9=9A=8F=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E4=B8=8B=E5=8F=91=E6=AD=A3=E5=BC=8F=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ docker-compose.yml | 3 ++- scripts/test-deployment-public-url.sh | 31 ++++++++++++++++++++++ system-update/release-manifest.json | 37 +++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 system-update/release-manifest.json diff --git a/README.md b/README.md index cd5276b..96bb11d 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,8 @@ update.sh 脚本用于自动更新 EasyAI 应用,包含以下功能: 更新执行器 `easyai-updater` 由 Watchtower 单独守护,默认每 24 小时检查并更新自身。它的任务状态保存在宿主机 `data/update-state` 中,自身更新或意外重启后会继续恢复未完成任务;Watchtower 不再接管三个核心服务,因此页面任务结果不会与后台静默替换相互冲突。 +受控更新使用仓库内的 `system-update/release-manifest.json` 作为稳定发布清单,Compose 会以只读方式挂载给 updater。发布新正式镜像时必须同步更新清单中的不可变摘要、镜像大小和版本标识;服务器执行 `update.sh` 拉取部署仓库后即可获得清单,不需要手工向运行目录复制文件。 + 存量部署无需手工编辑 `.env`。更新脚本会从 `scripts/update-env.d/010-managed-system-update.env` 应用以下缺省值: ```dotenv diff --git a/docker-compose.yml b/docker-compose.yml index c896cdf..8bc4b0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -338,10 +338,11 @@ services: - UPDATE_HEALTH_TIMEOUT_MS=${UPDATE_HEALTH_TIMEOUT_MS:-300000} - DOCKER_CONFIG_FILE=/run/secrets/docker-config.json - UPDATE_STATE_DIR=/var/lib/easyai-updater - - UPDATE_MANIFEST_FILE=/var/lib/easyai-updater/release-manifest.json + - UPDATE_MANIFEST_FILE=/etc/easyai-updater/release-manifest.json volumes: - /var/run/docker.sock:/var/run/docker.sock - ${DOCKER_CONFIG_FILE:-/root/.docker/config.json}:/run/secrets/docker-config.json:ro + - ./system-update/release-manifest.json:/etc/easyai-updater/release-manifest.json:ro - ./data/update-state:/var/lib/easyai-updater tmpfs: - /tmp:mode=1777 diff --git a/scripts/test-deployment-public-url.sh b/scripts/test-deployment-public-url.sh index cb9a301..c699f62 100755 --- a/scripts/test-deployment-public-url.sh +++ b/scripts/test-deployment-public-url.sh @@ -17,6 +17,7 @@ cp \ "$REPO_ROOT/easyai-proxy.conf.sample" \ "$TMP_DIR/" mkdir -p "$TMP_DIR/scripts" +mkdir -p "$TMP_DIR/system-update" cp \ "$REPO_ROOT/scripts/init-security-env.sh" \ "$REPO_ROOT/scripts/init-security-origin.sh" \ @@ -24,6 +25,7 @@ cp \ "$REPO_ROOT/scripts/init-server-http-bind-ip.sh" \ "$REPO_ROOT/scripts/check-redis-persistence.sh" \ "$TMP_DIR/scripts/" +cp "$REPO_ROOT/system-update/release-manifest.json" "$TMP_DIR/system-update/" cd "$TMP_DIR" @@ -166,6 +168,7 @@ mounts = {(item.get("source"), item.get("target")) for item in updater.get("volu assert ("/var/run/docker.sock", "/var/run/docker.sock") in mounts, mounts assert any(target == "/var/lib/easyai-updater" for _, target in mounts), mounts assert any(target == "/run/secrets/docker-config.json" for _, target in mounts), mounts +assert any(target == "/etc/easyai-updater/release-manifest.json" for _, target in mounts), mounts server_env = server.get("environment") or {} updater_env = updater.get("environment") or {} @@ -176,6 +179,7 @@ assert server_env.get("SYSTEM_UPDATE_MODE") == "managed", server_env.get("SYSTEM assert str(server_env.get("EASYAI_DEPLOYMENT_SCHEMA_VERSION")) == "2" assert str(updater_env.get("UPDATE_HEALTH_TIMEOUT_MS")) == "300000" assert updater_env.get("DOCKER_CONFIG_FILE") == "/run/secrets/docker-config.json" +assert updater_env.get("UPDATE_MANIFEST_FILE") == "/etc/easyai-updater/release-manifest.json" assert updater.get("labels", {}).get("com.centurylinklabs.watchtower.enable") == "true" assert watchtower.get("environment", {}).get("WATCHTOWER_POLL_INTERVAL") == "86400" assert watchtower.get("environment", {}).get("WATCHTOWER_CLEANUP") == "true" @@ -185,6 +189,33 @@ for service in ("easyai-server", "ws-gateway", "easyai-web"): labels = services[service].get("labels") or {} assert labels.get("com.centurylinklabs.watchtower.enable") == "false", (service, labels) ' + + python3 - <<'PY' +import json +import re +from pathlib import Path + +manifest = json.loads(Path("system-update/release-manifest.json").read_text()) +assert manifest["schemaVersion"] == 1 +assert manifest["channel"] == "stable" +assert manifest["platform"] == "linux/amd64" +assert manifest["managedUpdateAllowed"] is True +assert manifest["rollbackSafe"] is True +assert manifest["minimumUpdaterVersion"] == "1.0.0" +assert manifest["minimumDeploymentSchemaVersion"] == 2 +assert int(manifest["requiredFreeBytes"]) > 0 +components = {item["key"]: item for item in manifest["components"]} +assert set(components) == {"server-main", "ws-gateway", "web"} +expected_images = { + "server-main": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server", + "ws-gateway": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway", + "web": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai", +} +for key, image in expected_images.items(): + assert components[key]["image"] == image + assert re.fullmatch(r"sha256:[a-f0-9]{64}", components[key]["digest"]) + assert int(components[key]["imageSizeBytes"]) > 0 +PY } assert_legacy_update_contract() { diff --git a/system-update/release-manifest.json b/system-update/release-manifest.json new file mode 100644 index 0000000..a7c14da --- /dev/null +++ b/system-update/release-manifest.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 1, + "channel": "stable", + "releaseId": "managed-update-20260927", + "productVersion": "3.18.0.20260927", + "platform": "linux/amd64", + "publishedAt": "2026-09-27T11:15:00+08:00", + "managedUpdateAllowed": true, + "scheduledUpdateAllowed": true, + "rollbackSafe": true, + "minimumUpdaterVersion": "1.0.0", + "minimumDeploymentSchemaVersion": 2, + "requiredFreeBytes": 8589934592, + "components": [ + { + "key": "server-main", + "image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server", + "digest": "sha256:38cbe3fdb151b54dd729071e1368ad0670710a5b0160aa5fdbc486737bf5b2ef", + "imageSizeBytes": 2723428239, + "buildId": "db43ef28" + }, + { + "key": "ws-gateway", + "image": "registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway", + "digest": "sha256:661eb2dc5ca6e458e00b8c2e2f068c2a1773694a3017982ecb1aa42de677468e", + "imageSizeBytes": 1982899440, + "buildId": "db43ef28" + }, + { + "key": "web", + "image": "registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai", + "digest": "sha256:bc82b10e08886904b2ef4a5c2fe7727b1dac9114c7493b4f2acc0b1a8ef5e29c", + "imageSizeBytes": 1038564864, + "buildId": "188b0ee9" + } + ] +}