98 lines
4.5 KiB
Python
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))
|