"""Freeze the complete committed release; database transport is delivered separately.""" import argparse import hashlib import io import json from pathlib import Path import re import subprocess import zipfile ROOT = Path(__file__).resolve().parents[1] RELEASE = 'ksf-complete-deploy-20261106-r1' sha = lambda b: hashlib.sha256(b).hexdigest() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--commit', default='HEAD') args = parser.parse_args() commit = subprocess.check_output(['git', 'rev-parse', args.commit + '^{commit}'], cwd=ROOT).decode().strip() out = ROOT / 'artifacts/全部更新部署-20261106' work = ROOT / 'work' / RELEASE package = work / 'package' archive = out / (RELEASE + '.zip') if archive.exists() or package.exists(): raise ValueError('RELEASE_ALREADY_EXISTS') frozen = zipfile.ZipFile(io.BytesIO(subprocess.check_output(['git', 'archive', '--format=zip', commit], cwd=ROOT))) baseline = json.loads(frozen.read('deploy/board-files-20261106.json')) plan = json.loads(frozen.read('deploy/complete-release-plan.json')) for prior in plan['previousPushedCommits']: subprocess.run(['git', 'merge-base', '--is-ancestor', prior['commit'], commit], cwd=ROOT, check=True) static = {f['path'] for f in baseline['files']} package.mkdir(parents=True) for name in frozen.namelist(): if name.endswith('/'): continue if name in static or name == 'package.json' or name.startswith(('server/', 'skills/', 'scripts/', 'integrations/', 'test/')): target = package / 'payload' / name target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(frozen.read(name)) for f in baseline['files']: assert sha((package / 'payload' / f['path']).read_bytes()) == f['sha256'], 'STATIC_CONTENT_CHANGED_SINCE_BASELINE' baseline.update(commit=commit, release=RELEASE) (package / 'deploy').mkdir() (package / 'deploy/board-files.json').write_text(json.dumps(baseline, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') (package / 'deploy/complete-release-plan.json').write_bytes(frozen.read('deploy/complete-release-plan.json')) (package / 'deploy/complete-release-validation.json').write_bytes(frozen.read('deploy/complete-release-validation.json')) publisher = frozen.read('scripts/publish-opportunity-ui.mjs').decode('utf-8') start = publisher.index('export const FILES=Object.freeze([') end = publisher.index(']);', start) + 3 publisher = publisher[:start] + 'export const FILES=Object.freeze(' + json.dumps([f['path'] for f in baseline['files']]) + ');' + publisher[end:] publisher = publisher.replace('ksf.opportunity-ui.publish.v1', 'ksf.board.publish.v1').replace('ksf.opportunity-ui.20261103.v1', RELEASE) publisher = publisher.replace('filesChecked:6', 'filesChecked:FILES.length').replace('ksf-opportunity-ui', 'ksf-board') publisher = publisher.replace("sourceDir:root,manifestPath:path.join(root,'deploy/opportunity-ui-files-20261103.json')", "sourceDir:path.join(root,'payload'),manifestPath:path.join(root,'deploy/board-files.json')") (package / 'scripts').mkdir() (package / 'scripts/publish-board.mjs').write_text(publisher, encoding='utf-8', newline='\n') node = 'node' subprocess.run([node, '--check', str(package / 'scripts/publish-board.mjs')], check=True, capture_output=True) subprocess.run([node, 'server/scripts/build-member-groups.mjs'], cwd=package / 'payload', check=True, capture_output=True) docs = { '完整发布执行单.md': 'docs/board-deploy-20261106/完整发布执行单.md', '看板部署细则.md': 'docs/board-deploy-20261106/发给运维001-部署已推送看板.md', '数据库分包投递.md': 'docs/voc-database-delivery-20261106/发给运维001-数据库分包投递.md', '王刚001-数据库就绪后验收.md': 'docs/voc-database-delivery-20261106/给王刚001-数据库就绪后继续验收.md', } for name, source in docs.items(): body = frozen.read(source).decode('utf-8') if name == '完整发布执行单.md': body = f'> 本包固定源码提交:{commit}\n\n' + body (package / name).write_text(body, encoding='utf-8', newline='\n') functions = json.loads((package / 'payload/server/dist/member-groups/manifest.json').read_text()) skills = {} for folder in (package / 'payload/skills').iterdir(): manifest = folder / 'FILES.sha256' for line in manifest.read_text(encoding='utf-8').splitlines(): h, rel = line.split(' ', 1) assert sha((folder / rel).read_bytes()) == h, 'SKILL_CHECKSUM_MISMATCH' skills[folder.name] = {'manifestSha256': sha(manifest.read_bytes()), 'filesVerified': len(manifest.read_text().splitlines())} imported = 0 for relative in static: p = package / 'payload' / relative if p.suffix != '.js': continue for m in re.finditer(r'''(?:from|import)\s*\(?\s*['"](\.[^'"]+)['"]''', p.read_text(encoding='utf-8')): target = (p.parent / m[1]).resolve() assert target.is_relative_to((package / 'payload/src').resolve()) and target.is_file(), 'MODULE_DEPENDENCY_MISSING' imported += 1 expected_files = [p for p in package.rglob('*') if p.is_file()] assert not any(p.suffix.lower() in {'.sqlite', '.db', '.zip', '.pem', '.key', '.pyc'} or p.name.startswith('.env') or p.name == 'credential-binding.json' for p in expected_files) assert not (package / 'payload/index.html').exists() release = {'release': RELEASE, 'sourceCommit': commit, 'previousPushedCommits': plan['previousPushedCommits'], 'staticFiles': len(static), 'relativeImportsVerified': imported, 'functions': functions, 'skills': skills, 'databaseIncluded': False, 'credentialsIncluded': False, 'databaseTransportRequired': ['ksf-voc-database-20261106-r1-part01.zip', 'ksf-voc-database-20261106-r1-part02.zip'], 'productionInstalled': False, 'browserVerified': False, 'productionWrites': 0} (package / 'RELEASE.json').write_text(json.dumps(release, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') all_files = sorted(p for p in package.rglob('*') if p.is_file()) (package / 'FILES.sha256').write_text(''.join(sha(p.read_bytes()) + ' ' + p.relative_to(package).as_posix() + '\n' for p in all_files), encoding='utf-8', newline='\n') out.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED, compresslevel=9) as z: for p in [*all_files, package / 'FILES.sha256']: z.write(p, p.relative_to(package).as_posix()) with zipfile.ZipFile(archive) as z: assert z.testzip() is None for line in z.read('FILES.sha256').decode('utf-8').splitlines(): h, name = line.split(' ', 1) assert sha(z.read(name)) == h receipt = {'ok': True, 'filename': archive.name, 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes()), 'entries': len(all_files) + 1, 'filesVerified': len(all_files), **release} (out / 'delivery-receipt.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') (out / '完整发布执行单.md').write_bytes((package / '完整发布执行单.md').read_bytes()) print(json.dumps(receipt, ensure_ascii=False, indent=2)) if __name__ == '__main__': main()