finalize-board-handoff.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. """Seal the reviewed board handoff, validate its hashes, and emit a delivery receipt."""
  2. import hashlib
  3. import json
  4. from pathlib import Path
  5. import re
  6. import shutil
  7. import zipfile
  8. ROOT = Path(__file__).resolve().parents[1]
  9. BASE = ROOT / 'work/board-deploy-20261106-r1'
  10. PKG = BASE / 'package'
  11. OUT = ROOT / 'artifacts/看板部署-20261106'
  12. sha = lambda b: hashlib.sha256(b).hexdigest()
  13. def finalize():
  14. OUT.mkdir(parents=True, exist_ok=True)
  15. dest = OUT / 'ksf-board-deploy-20261106-r1.zip'
  16. if dest.exists():
  17. raise RuntimeError('RELEASE_ALREADY_SEALED')
  18. doc = ROOT / 'docs/board-deploy-20261106/发给运维001-部署已推送看板.md'
  19. shutil.copyfile(doc, PKG / doc.name)
  20. manifest = json.loads((PKG / 'deploy/board-files.json').read_text(encoding='utf-8'))
  21. closure = []
  22. for f in manifest['files']:
  23. p = PKG / 'payload' / f['path']
  24. assert sha(p.read_bytes()) == f['sha256'], f['path']
  25. if p.suffix == '.js':
  26. for m in re.finditer(r'''(?:from|import)\s*\(?\s*['"](\.[^'"]+)['"]''', p.read_text(encoding='utf-8')):
  27. imported = (p.parent / m[1]).resolve()
  28. assert imported.is_relative_to((PKG / 'payload/src').resolve()) and imported.is_file(), (p, m[1])
  29. closure.append(str(imported.relative_to(PKG)))
  30. assert not (PKG / 'payload/index.html').exists()
  31. assert not (PKG / 'payload/src/dev').exists()
  32. assert not list(PKG.rglob('*.sqlite'))
  33. assert not list(PKG.rglob('*.pyc'))
  34. assert not list(PKG.rglob('.env*'))
  35. publisher = json.loads((BASE / 'static-publisher-validation.json').read_text())
  36. assert publisher['ok']
  37. validation = {'ok': True, 'sourceCommit': manifest['commit'], 'staticFilesVerified': 71,
  38. 'relativeImportsChecked': len(closure), 'nodeTests': {'total': 83, 'passed': 83, 'failed': 0},
  39. 'pythonTests': {'total': 50, 'passed': 50, 'failed': 0, 'skipped': 0, 'usesPrivateCorpus': True},
  40. 'publisher': publisher, 'productionWrites': 0, 'productionBrowserVerified': False}
  41. (PKG / 'VALIDATION.json').write_text(json.dumps(validation, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  42. files = sorted(p for p in PKG.rglob('*') if p.is_file() and p.name != 'FILES.sha256')
  43. lines = [sha(p.read_bytes()) + ' ' + p.relative_to(PKG).as_posix() for p in files]
  44. (PKG / 'FILES.sha256').write_text('\n'.join(lines) + '\n', encoding='utf-8', newline='\n')
  45. with zipfile.ZipFile(dest, 'x', zipfile.ZIP_DEFLATED, compresslevel=9) as z:
  46. for p in [*files, PKG / 'FILES.sha256']:
  47. z.write(p, p.relative_to(PKG).as_posix())
  48. with zipfile.ZipFile(dest) as z:
  49. assert z.testzip() is None
  50. for line in z.read('FILES.sha256').decode('utf-8').splitlines():
  51. digest, p = line.split(' ', 1)
  52. assert sha(z.read(p)) == digest, p
  53. assert len(z.namelist()) == len(files) + 1
  54. release = json.loads((PKG / 'RELEASE.json').read_text(encoding='utf-8'))
  55. receipt = {'ok': True, 'file': dest.name, 'bytes': dest.stat().st_size, 'sha256': sha(dest.read_bytes()),
  56. 'entries': len(files) + 1, 'filesVerified': len(files), 'release': release['release'], 'commit': release['commit'],
  57. 'requiresDatabasePackage': release['requiredDatabasePackage'], 'productionInstalled': False,
  58. 'productionWrites': 0, 'browserVerified': False, 'validation': validation}
  59. (OUT / 'delivery-receipt.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  60. shutil.copyfile(doc, OUT / doc.name)
  61. print(json.dumps(receipt, ensure_ascii=False))
  62. if __name__ == '__main__':
  63. finalize()