#!/usr/bin/env python3 """Adopt a real non-Git directory and resolve the resulting Compose with Docker CLI.""" import json import os from pathlib import Path import shutil import subprocess import tempfile import yaml source = Path(__file__).resolve().parent.parent def run(arguments, root, check=True): return subprocess.run(arguments, cwd=root, capture_output=True, text=True, check=check, timeout=40) with tempfile.TemporaryDirectory(prefix='easyai-package-adoption-') as temporary: base = Path(temporary) template = base / 'tool' template.mkdir() for name in ['docker-compose.yml', '.gitignore']: shutil.copy2(source / name, template / name) for sample in source.glob('.env*.sample'): shutil.copy2(sample, template / sample.name) for command in [['git','init','-q'], ['git','config','user.name','Deployment fixture'], ['git','config','user.email','fixture@example.invalid'], ['git','add','.'], ['git','commit','-qm','test: 部署模板'], ['git','remote','add','origin','https://example.invalid/deployment.git']]: run(command, template) for kind in ('compatible', 'conflicting-port'): project = base / kind project.mkdir() for sample in source.glob('.env*.sample'): shutil.copy2(sample, project / sample.name.removesuffix('.sample')) private = (project / '.env').read_text() + '\nEXISTING_PRIVATE_SETTING=must-stay-private\n' (project / '.env').write_text(private) original = yaml.safe_load((source / 'docker-compose.yml').read_text()) original['name'] = 'original-customer-project' original['services']['easyai-server']['environment'].append('EXISTING_OPTION=keep') if kind == 'conflicting-port': original['services']['easyai-web']['ports'] = ['127.0.0.1:39111:3010'] (project / 'docker-compose.yml').write_text(yaml.safe_dump(original, sort_keys=False)) marker = project / 'data/upload/existing-file.txt' marker.parent.mkdir(parents=True) marker.write_text('existing upload must remain') original_compose = (project / 'docker-compose.yml').read_bytes() result = run(['python3', str(source / 'scripts/adopt-deployment-directory.py'), str(project), str(template)], project, check=False) assert 'must-stay-private' not in result.stdout + result.stderr assert marker.read_text() == 'existing upload must remain' if kind == 'compatible': assert result.returncode == 0, result.stdout + result.stderr assert (project / '.env').read_text().startswith(private) assert (project / '.git').is_dir() resolved = json.loads(run(['docker','compose','config','--format','json'], project).stdout) assert resolved['name'] == 'original-customer-project' assert resolved['services']['easyai-server']['environment']['EXISTING_OPTION'] == 'keep' assert len(resolved['services']['easyai-web']['ports']) == 1 backups = list((project / 'data/deployment-backups').glob('package-*')) assert len(backups) == 1 assert (backups[0] / 'files/.env').read_text() == private assert (backups[0] / 'files/docker-compose.yml').read_bytes() == original_compose assert os.stat(backups[0]).st_mode & 0o777 == 0o700 assert not run(['git','status','--porcelain'], project).stdout else: assert result.returncode != 0 and '端口' in result.stdout + result.stderr assert not (project / '.git').exists() assert (project / '.env').read_text() == private assert (project / 'docker-compose.yml').read_bytes() == original_compose print(kind + ': data/config preserved and unsafe merge rejected before mutation')