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
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Refuse to adopt another Compose project or replace existing persistent storage."""
import argparse
import json
import os
from pathlib import Path
import re
import subprocess
import sys
STATEFUL = {'mongo': {'/data/db'}, 'redis': {'/data'}, 'rabbitmq': {'/var/lib/rabbitmq'},
'easyai-pgvector': {'/var/lib/postgresql', '/var/lib/postgresql/data'},
'easyai-server': {'/app/upload'}}
def canonical(path):
path = path.replace('\\', '/')
path = re.sub(r'^/(?:run/desktop/mnt/host|host_mnt)/([A-Za-z])/', r'\1:/', path)
if re.match(r'^[A-Za-z]:/', path):
return path.rstrip('/').casefold()
if path.startswith('/host_mnt/'):
path = path[len('/host_mnt'):]
return str(Path(path).resolve()).rstrip('/')
def validate(configuration, existing, selected=()):
for service, settings in configuration['services'].items():
if selected and service not in selected:
continue
container = existing.get(service)
if not container:
continue
project = (container.get('Config', {}).get('Labels') or {}).get('com.docker.compose.project')
if project != configuration['name']:
raise ValueError(f'{service} 的现有容器不属于当前 Compose 项目;请核对原安装目录和项目名')
actual = {mount['Destination']: mount for mount in container.get('Mounts', [])}
for mount in settings.get('volumes', []):
if mount['target'] not in STATEFUL.get(service, set()):
continue
old = actual.get(mount['target'], {})
if mount['type'] == 'bind':
expected = canonical(mount['source'])
equal = old.get('Type') == 'bind' and canonical(old.get('Source', '')) == expected and Path(expected).is_dir()
else:
definition = configuration.get('volumes', {}).get(mount.get('source'), {})
name = definition.get('name')
equal = bool(name) and old.get('Type') == 'volume' and old.get('Name') == name
if not equal:
raise ValueError(f'{service} 的 {mount["target"]} 数据位置将改变;请先备份并按数据迁移步骤处理')
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)
docker = ['sudo', 'docker'] if args.sudo else ['docker']
if subprocess.run(docker + ['info'], capture_output=True).returncode:
raise ValueError('无法连接 Docker,未检查现有容器和数据位置')
existing = {}
for service, settings in configuration['services'].items():
if args.services and service not in args.services:
continue
# Explicit global names can collide with another installation. Services
# without container_name are already scoped by Compose's project name.
if not settings.get('container_name'):
continue
result = subprocess.run(docker + ['container', 'inspect', settings['container_name']], capture_output=True, text=True)
if result.returncode:
if result.stderr and not re.search('No such (?:container|object)', result.stderr, re.I):
raise ValueError(f'无法核对现有容器 {service},未继续部署')
continue
existing[service] = json.loads(result.stdout)[0]
validate(configuration, existing, args.services)
print('✅ 现有容器归属和持久化数据位置检查通过')
if __name__ == '__main__':
try:
main()
except (ValueError, KeyError, TypeError, OSError) as error:
print('❌ ' + str(error) + ';尚未拉取或重建服务', file=sys.stderr)
sys.exit(1)