package-enterprise-workshop.py 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. """Build an enterprise bootstrap package from the unchanged sealed r13 and runtime hotfix."""
  2. import argparse
  3. import ast
  4. import hashlib
  5. import importlib.util
  6. import io
  7. import json
  8. from pathlib import Path
  9. import sys
  10. import zipfile
  11. sys.dont_write_bytecode = True
  12. ROOT = Path(__file__).resolve().parents[1]
  13. SOURCE = ROOT / 'deploy/enterprise-workshop'
  14. NAME = 'ksf-enterprise-workshop-20261110-r2'
  15. sha = lambda raw: hashlib.sha256(raw).hexdigest()
  16. def need(ok, code):
  17. if not ok:
  18. raise ValueError(code)
  19. def content():
  20. files = {p.name: p.read_bytes() for p in SOURCE.iterdir() if p.is_file()}
  21. files['.gitattributes'] = b'* -text\n'
  22. cfg = json.loads(files['enterprise.json'])
  23. skill_archive = ROOT / 'docs/ksf-workshop-skill-20261110-r13-r2/publish/ksf-workshop-skill-20261110-r13-r2.zip'
  24. hotfix_archive = ROOT / 'docs/ksf-unified-runtime-hotfix-20261109-r1/publish/ksf-unified-runtime-hotfix-20261109-r1.zip'
  25. need(sha(skill_archive.read_bytes()) == cfg['skill']['upstreamArchiveSha256'], 'SEALED_SKILL_CHANGED')
  26. need(sha(hotfix_archive.read_bytes()) == cfg['researchRuntime']['upstreamArchiveSha256'], 'SEALED_HOTFIX_CHANGED')
  27. with zipfile.ZipFile(skill_archive) as z:
  28. meta = json.loads(z.read('release.json'))
  29. for name, digest in meta['workshop']['files'].items():
  30. raw = z.read('skills/ksf-workshop-runtime/' + name)
  31. need(sha(raw) == digest, 'SKILL_HASH_MISMATCH')
  32. files['payload/skills/ksf-workshop-runtime/' + name] = raw
  33. with zipfile.ZipFile(hotfix_archive) as z:
  34. prefix = 'components/research-runtime/'
  35. for line in z.read(prefix + 'FILES.sha256').decode().splitlines():
  36. digest, name = line.split(' ', 1)
  37. need(name.startswith('scripts/'), 'UNEXPECTED_RESEARCH_COMPONENT')
  38. raw = z.read(prefix + name)
  39. need(sha(raw) == digest, 'RESEARCH_COMPONENT_HASH_MISMATCH')
  40. files['payload/skills/ksf-voc-library/' + name] = raw
  41. for name, raw in files.items():
  42. if name.endswith('.py'):
  43. ast.parse(raw.decode('utf-8-sig'), filename=name, feature_version=(3, 9))
  44. files['FILES.sha256'] = ''.join(sha(b) + ' ' + n + '\n' for n, b in sorted(files.items())).encode()
  45. return files
  46. def build(output):
  47. need(not output.exists(), 'NEW_OUTPUT_REQUIRED')
  48. files = content()
  49. for name, raw in files.items():
  50. file = output / name
  51. file.parent.mkdir(parents=True, exist_ok=True)
  52. file.write_bytes(raw)
  53. return files
  54. def main():
  55. parser = argparse.ArgumentParser(description=__doc__)
  56. parser.add_argument('--out', type=Path, required=True)
  57. args = parser.parse_args()
  58. files = build(args.out / 'package')
  59. stream = io.BytesIO()
  60. with zipfile.ZipFile(stream, 'w', zipfile.ZIP_DEFLATED) as z:
  61. for name, raw in sorted(files.items()):
  62. entry = zipfile.ZipInfo(name, (2026, 11, 10, 0, 0, 0))
  63. entry.external_attr = 0o100644 << 16
  64. z.writestr(entry, raw, compress_type=zipfile.ZIP_DEFLATED)
  65. archive = stream.getvalue()
  66. (args.out / (NAME + '.zip')).write_bytes(archive)
  67. receipt = {'release': NAME, 'archive': NAME + '.zip', 'bytes': len(archive), 'sha256': sha(archive),
  68. 'files': len(files), 'skillVersion': 'r13', 'existingSkillFilesChanged': 0,
  69. 'credentialsIncluded': False, 'productionDeployed': False,
  70. 'vocVersion': '0.3.44', 'vocRequired': True, 'thirdPartyVocArchiveIncluded': False,
  71. 'automaticLifecycleHookInstalled': False, 'businessWrites': 0}
  72. (args.out / 'release.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  73. print(json.dumps(receipt, ensure_ascii=False, indent=2))
  74. if __name__ == '__main__':
  75. main()