| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- """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()
|