Files
2026-09-29 15:52:07 +08:00

98 lines
4.5 KiB
Python

"""Assemble the server bundle locally: payload + manifest + tar.gz. No upload, no deployment.
python prepare_bundle.py
Sources: C:/kefu/wechat_rpa/remote_api.py and the rebuilt C:/kefu/admin-web/apps/mobile-h5/dist.
Production for these files is exactly the mobile-remote-20260928 payload, so the replaced file is
diffed against that payload and the diff is written to payload-diff.txt for review.
"""
from pathlib import Path
import difflib, hashlib, json, shutil, tarfile
folder = Path(__file__).resolve().parent
kefu = folder.parent.parent
previous = folder.parent / 'mobile-remote-20260928' / 'payload'
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')
frontend = plan['frontendDir']
BANNER = '只能在公司内网登录'.encode()
# 1. The recorded baseline is what the previous release put on the server.
for name, wanted in base['expected'].items():
assert sha((previous / name).read_bytes()) == wanted, 'Baseline differs from the deployed payload: ' + name
# 2. Fresh payload from the working trees; record the backend diff.
payload = folder / 'payload'
if payload.exists():
shutil.rmtree(payload)
diff_lines = []
for name in plan['backendFiles'] + plan['newBackendFiles'] + plan['deployFiles']:
data = lf((kefu / name).read_bytes())
assert b'\r' not in data, 'Stray carriage return: ' + name
if name.endswith('.py'):
compile(data, name, 'exec')
before = previous / name
if before.exists():
diff_lines += difflib.unified_diff(before.read_text(encoding='utf-8').splitlines(),
data.decode('utf-8').splitlines(),
'production/' + name, 'release/' + name, lineterm='')
target = payload / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
(folder / 'payload-diff.txt').write_text('\n'.join(diff_lines) + '\n', encoding='utf-8')
dist = kefu / frontend
assert (dist / 'index.html').is_file(), 'Build the phone console first: cd admin-web/apps/mobile-h5 && pnpm build'
for text in dist.rglob('*'):
if text.is_file():
data = text.read_bytes()
if text.suffix in {'.html', '.js', '.css', '.json', '.webmanifest'}:
assert b'\r\n' not in data, 'CRLF in build output: ' + str(text)
assert BANNER not in data, 'Old intranet-only banner still in build: ' + str(text)
shutil.copytree(dist, payload / frontend)
tests = folder / 'tests'
if tests.exists():
shutil.rmtree(tests)
tests.mkdir()
for module in plan['serverTestModules']:
data = lf((kefu / 'wechat_rpa' / f'{module}.py').read_bytes())
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-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(lf((folder / name).read_bytes()))
archive = folder / 'mobile-http-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), 'diff_lines': len(diff_lines), 'uploaded': False}, ensure_ascii=False, indent=1))