fix(deploy): 修复组件版本解析和安装升级验收流程
Test deployment public URL (Linux) / test-linux (push) Waiting to run
Test start.ps1 (Windows) / test-windows (push) Waiting to run

This commit is contained in:
2026-10-18 00:18:00 +08:00
parent 895d92317e
commit b99eb849b4
30 changed files with 1579 additions and 270 deletions
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Verify the resolved installation images, without pulling layers or using ACR credentials."""
import argparse
from concurrent.futures import ThreadPoolExecutor
import json
import subprocess
import sys
import tempfile
STABLE_KEYS = {'easyai-server': 'SERVER_STABLE_DIGEST', 'easyai-web': 'WEB_STABLE_DIGEST', 'ws-gateway': 'WS_GATEWAY_STABLE_DIGEST'}
def verify(image, docker, config, expected=None):
# Official installation images must be available to a new anonymous user.
# Never reuse a maintainer's credential helper as evidence of public access.
official = image.startswith(('registry.cn-shanghai.aliyuncs.com/comfy-ai/',
'registry.cn-shanghai.aliyuncs.com/easyaigc/'))
command = docker + (['--config', config] if official else [])
try:
result = subprocess.run(command + ['manifest', 'inspect', '--verbose', image],
capture_output=True, text=True, timeout=90)
if result.returncode:
message = result.stderr.lower()
reason = ('标签或摘要不存在' if any(s in message for s in ('not found', 'manifest unknown', 'no such manifest'))
else '未开放匿名拉取' if any(s in message for s in ('unauthorized', 'denied', 'authentication required'))
else '仓库不可访问,请检查网络或仓库状态')
raise ValueError(reason)
manifests = json.loads(result.stdout)
manifests = manifests if isinstance(manifests, list) else [manifests]
platforms = [m.get('Descriptor', {}).get('platform', {}) for m in manifests]
if not any(p.get('os') == 'linux' and p.get('architecture') == 'amd64' for p in platforms):
raise ValueError('没有 linux/amd64 产物')
if expected and not any(m.get('Descriptor', {}).get('digest') == expected
and m.get('Descriptor', {}).get('platform', {}).get('architecture') == 'amd64'
for m in manifests):
raise ValueError('官方稳定别名与发布清单尚未同步;保留当前服务')
return image, None
except (subprocess.TimeoutExpired, OSError):
return image, '镜像检查超时或 Docker 不可用'
except (ValueError, KeyError, TypeError) as error:
return image, str(error) if isinstance(error, ValueError) and not isinstance(error, json.JSONDecodeError) else '仓库返回了无效的镜像信息'
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--sudo', action='store_true')
parser.add_argument('--services', nargs='*', default=[])
args = parser.parse_args()
configuration = json.load(sys.stdin)
images = sorted({value['image'] for service, value in configuration['services'].items()
if not args.services or service in args.services})
expected = {value['image']: configuration.get('x-easyai-release', {}).get(service)
for service, value in configuration['services'].items() if service in STABLE_KEYS}
if not images:
parser.error('没有可检查的部署镜像')
docker = ['sudo', 'docker'] if args.sudo else ['docker']
with tempfile.TemporaryDirectory(prefix='easyai-anonymous-registry-') as config:
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(lambda image: verify(image, docker, config, expected.get(image)), images))
failures = [(image, error) for image, error in results if error]
for image, error in failures:
print(f'❌ {image}: {error}', file=sys.stderr)
if failures:
print('镜像检查失败,尚未拉取或重建服务;请核对发布记录和组件实际版本。', file=sys.stderr)
return 1
print(f'✅ 已核验 {len(images)} 个部署镜像的可访问性和 linux/amd64 平台')
return 0
if __name__ == '__main__':
sys.exit(main())