99 lines
4.9 KiB
Python
99 lines
4.9 KiB
Python
"""Assemble the server bundle locally: payload + manifest + tar.gz. No upload, no deployment.
|
|
|
|
python prepare_bundle.py
|
|
|
|
Sources come from the working trees under C:/kefu (backend in wechat_rpa, the phone
|
|
console build in admin-web/apps/mobile-h5/dist). baseline-snapshot/ holds the exact bytes
|
|
production runs; every change production carries on top of git HEAD must already be in
|
|
the payload, so a lagging local copy cannot roll production back.
|
|
"""
|
|
from pathlib import Path
|
|
import hashlib, json, shutil, subprocess, tarfile, tempfile
|
|
|
|
folder = Path(__file__).resolve().parent
|
|
kefu = folder.parent.parent
|
|
plan = json.loads((folder / 'release-plan.json').read_text(encoding='utf-8'))
|
|
base = json.loads((folder / 'server-baseline.json').read_text(encoding='utf-8'))
|
|
sha = lambda data: hashlib.sha256(data).hexdigest()
|
|
lf = lambda data: data.replace(b'\r\n', b'\n')
|
|
|
|
# 1. Production is not git HEAD (the 2026-09-23 cos-direct release shipped admin_api.py from
|
|
# another working tree), so merge HEAD -> production into the payload: it must be a no-op.
|
|
for name, wanted in base['expected'].items():
|
|
snapshot = (folder / 'baseline-snapshot' / name).read_bytes()
|
|
assert sha(snapshot) == wanted, 'baseline-snapshot differs from server-baseline.json: ' + name
|
|
ours = lf((kefu / name).read_bytes())
|
|
committed = lf(subprocess.check_output(['git', '-C', str(kefu), 'show', f'HEAD:{name}']))
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
paths = []
|
|
for label, data in (('ours', ours), ('head', committed), ('production', lf(snapshot))):
|
|
(Path(tmp) / label).write_bytes(data)
|
|
paths.append(str(Path(tmp) / label))
|
|
merged = subprocess.run(['git', 'merge-file', '-p', *paths], capture_output=True)
|
|
assert merged.returncode == 0 and merged.stdout == ours, 'Payload is missing production changes: ' + name
|
|
# The phone console is served as-is; make sure the build did not pick up CRLF from Windows.
|
|
for text in (kefu / plan['frontendDir']).rglob('*'):
|
|
if text.is_file() and text.suffix in {'.html', '.js', '.css', '.json', '.webmanifest'}:
|
|
assert b'\r\n' not in text.read_bytes(), 'CRLF in build output: ' + str(text)
|
|
|
|
# 2. Fresh payload from the working trees.
|
|
payload = folder / 'payload'
|
|
if payload.exists():
|
|
shutil.rmtree(payload)
|
|
for name in plan['backendFiles'] + plan['newBackendFiles'] + plan['deployFiles']:
|
|
# Windows 工作区是 autocrlf 检出的 CRLF;仓库和生产服务器上都是 LF。
|
|
data = (kefu / name).read_bytes().replace(b'\r\n', b'\n')
|
|
assert b'\r' not in data, 'Stray carriage return: ' + name
|
|
if name.endswith('.py'):
|
|
compile(data, name, 'exec')
|
|
target = payload / name
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_bytes(data)
|
|
dist = kefu / plan['frontendDir']
|
|
assert (dist / 'index.html').is_file(), 'Build the phone console first: cd admin-web/apps/mobile-h5 && pnpm build'
|
|
shutil.copytree(dist, payload / plan['frontendDir'])
|
|
|
|
tests = folder / 'tests'
|
|
if tests.exists():
|
|
shutil.rmtree(tests)
|
|
tests.mkdir()
|
|
for module in plan['serverTestModules']:
|
|
data = (kefu / 'wechat_rpa' / f'{module}.py').read_bytes().replace(b'\r\n', b'\n')
|
|
compile(data, module, 'exec')
|
|
(tests / f'{module}.py').write_bytes(data)
|
|
|
|
scripts = ['release-plan.json', 'server-baseline.json', 'Dockerfile.overlay', 'remote_server_guard.py',
|
|
'01-preflight.sh', '02-backup-build.sh', '03-deploy.sh', '04-verify.sh', 'rollback.sh']
|
|
|
|
# 3. Manifest over everything the server will verify.
|
|
manifest = []
|
|
for path in sorted(p for p in list(payload.rglob('*')) + list(tests.rglob('*')) if p.is_file()):
|
|
data = path.read_bytes()
|
|
manifest.append({'path': path.relative_to(folder).as_posix(), 'sha256': sha(data), 'bytes': len(data)})
|
|
(folder / 'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8')
|
|
|
|
# 4. Server bundle (LF line endings for shell scripts) and archive.
|
|
bundle = folder / 'server-bundle'
|
|
if bundle.exists():
|
|
shutil.rmtree(bundle)
|
|
bundle.mkdir()
|
|
shutil.copytree(payload, bundle / 'payload')
|
|
shutil.copytree(tests, bundle / 'tests')
|
|
for name in scripts + ['manifest.json']:
|
|
(bundle / name).write_bytes((folder / name).read_bytes().replace(b'\r\n', b'\n'))
|
|
archive = folder / 'mobile-remote-server-bundle.tar.gz'
|
|
if archive.exists():
|
|
archive.unlink()
|
|
with tarfile.open(archive, 'x:gz') as tf:
|
|
for p in sorted(bundle.rglob('*')):
|
|
if p.is_file():
|
|
arc = p.relative_to(bundle).as_posix(); assert not arc.startswith(('/', '..'))
|
|
info = tf.gettarinfo(str(p), arcname=arc)
|
|
info.mode = 0o755 if arc.endswith('.sh') else 0o644
|
|
info.uid = info.gid = 0
|
|
info.uname = info.gname = ''
|
|
with p.open('rb') as handle:
|
|
tf.addfile(info, handle)
|
|
print(json.dumps({'archive': str(archive), 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes()),
|
|
'files': len(manifest), 'uploaded': False}, ensure_ascii=False, indent=1))
|