From f01d36d2b83f6d52606f3cce058599d6ef55c688 Mon Sep 17 00:00:00 2001 From: easyai02 Date: Fri, 9 Oct 2026 03:16:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E4=BF=9D=E7=95=99=E5=B7=B2?= =?UTF-8?q?=E6=9C=89=E8=A6=86=E7=9B=96=E9=85=8D=E7=BD=AE=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=87=8D=E5=A4=8D=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/prepare-update-repository.py | 31 +++++++++++++++++++++--- scripts/test-repository-image-restore.py | 16 ++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/scripts/prepare-update-repository.py b/scripts/prepare-update-repository.py index dea341f..741866b 100644 --- a/scripts/prepare-update-repository.py +++ b/scripts/prepare-update-repository.py @@ -150,6 +150,29 @@ if token_key in old_monitor and token_key not in monitor and token_key in new_mo monitor[token_key] = new_monitor[token_key] merged = merge(base, local, remote) override = delta(remote, merged) +config_dir = root / 'data/deployment-config' +override_path = config_dir / 'legacy-compose.json' + +def apply_override(original, additional): + if isinstance(original, dict) and isinstance(additional, dict): + result = copy.deepcopy(original) + for key, value in additional.items(): + result[key] = apply_override(result.get(key, MISSING), value) + return result + return copy.deepcopy(additional) + +# Repeated adoption must preserve already migrated settings. A newly reviewed +# main-file difference has the same precedence as the old main file; only +# explicitly restored target images are released back to the repository. +if override_path.exists(): + if override_path.is_symlink() or not override_path.is_file(): + sys.exit('❌ 旧配置覆盖文件类型不受支持;尚未修改配置或容器') + previous_override = canonical(json.loads(override_path.read_text())) + for service in restore_images: + previous_override.get('services', {}).get(service, {}).pop('image', None) + override = apply_override(previous_override, {} if override is MISSING else override) + merged = apply_override(remote, override) + with tempfile.NamedTemporaryFile(mode='w', dir=root, prefix='.update-compose-', suffix='.json', delete=False) as candidate: json.dump(serialize(merged), candidate) candidate_path = Path(candidate.name) @@ -165,7 +188,9 @@ backup_root = root / 'data/deployment-backups' backup_root.mkdir(parents=True, exist_ok=True, mode=0o700) backup = Path(tempfile.mkdtemp(prefix='legacy-update-' + time.strftime('%Y%m%d-%H%M%S') + '-', dir=backup_root)) backup.chmod(0o700) -for name in changed | collisions | {'.env'}: +backup_files = changed | collisions | {'.env'} +if override_path.exists(): backup_files.add(str(override_path.relative_to(root))) +for name in backup_files: destination = backup / name destination.parent.mkdir(parents=True, exist_ok=True, mode=0o700) shutil.copy2(root / name, destination) @@ -174,8 +199,6 @@ config_dir = root / 'data/deployment-config' config_dir.mkdir(parents=True, exist_ok=True, mode=0o700) override_path = config_dir / 'legacy-compose.json' if override is not MISSING and override: - if override_path.exists(): - sys.exit('❌ 已存在旧配置迁移结果;请检查备份后再重试,尚未修改仓库') override_path.write_text(json.dumps(serialize(override), indent=2) + '\n') override_path.chmod(0o600) env_path = root / '.env' @@ -192,7 +215,7 @@ for name in collisions: (root / name).unlink() if changed: subprocess.run(['git', 'restore', '--source=HEAD', '--staged', '--worktree', '--', 'docker-compose.yml'], check=True) -(backup / 'migration.json').write_text(json.dumps({'from': git('rev-parse', 'HEAD').strip(), 'to': git('rev-parse', upstream).strip(), 'preservedFields': preserved, 'restoredRepositoryImages': sorted(restore_images), 'files': sorted(changed | collisions)}, indent=2)) +(backup / 'migration.json').write_text(json.dumps({'from': git('rev-parse', 'HEAD').strip(), 'to': git('rev-parse', upstream).strip(), 'preservedFields': preserved, 'restoredRepositoryImages': sorted(restore_images), 'files': sorted(backup_files)}, indent=2)) (backup / 'migration.json').chmod(0o600) print('✅ 已备份旧配置并迁移本地差异;新版部署字段由仓库继续管理') print(' 备份目录: ' + str(backup)) diff --git a/scripts/test-repository-image-restore.py b/scripts/test-repository-image-restore.py index e5085e9..d349a5e 100644 --- a/scripts/test-repository-image-restore.py +++ b/scripts/test-repository-image-restore.py @@ -86,6 +86,22 @@ for restore in ('', 'easyai-web', 'redis', 'ws-gateway'): # A second adoption preserves the result and creates no extra backup. checked(['python3', str(prepare), str(root)], root, env) assert len(list((root / 'data/deployment-backups').glob('legacy-update-*'))) == 1 + # A later main-file customization is adopted without dropping the + # existing override or changing an unrelated dependency setting. + main_path = root / 'docker-compose.yml' + main_path.write_text(main_path.read_text().replace('LOCAL_SETTING=original', 'LOCAL_SETTING=second-custom')) + override_file = root / 'data/deployment-config/legacy-compose.json' + previous_override = override_file.read_bytes() + checked(['python3', str(prepare), str(root)], root, env) + config = json.loads(checked(['docker', 'compose', '--env-file', '.env', 'config', '--format', 'json'], root)) + assert config['services']['easyai-server']['environment']['LOCAL_SETTING'] == 'second-custom' + assert config['services']['redis']['image'] == 'redis:6.2' + assert config['services']['easyai-web']['image'] == ('repository-web:latest' if restore else pin) + backups = list((root / 'data/deployment-backups').glob('legacy-update-*')) + assert len(backups) == 2 + newest = max(backups, key=lambda p: p.stat().st_mtime) + assert (newest / 'data/deployment-config/legacy-compose.json').read_bytes() == previous_override + assert (newest / 'data/deployment-config/legacy-compose.json').stat().st_mode & 0o077 == 0 for mode, adopt in (('image', '1'), ('full', '0')): env = {**os.environ, 'UPDATE_MODE': mode, 'UPDATE_ADOPT_LEGACY_CONFIG': adopt, 'UPDATE_RESTORE_REPOSITORY_IMAGES': 'easyai-web'}