package-workshop-onboarding.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. """Package a complete first-install kit; preserve sealed incremental releases."""
  2. from pathlib import Path
  3. import argparse
  4. import hashlib
  5. import io
  6. import json
  7. import re
  8. import tarfile
  9. import zipfile
  10. ROOT = Path(__file__).resolve().parents[1]
  11. OUT = ROOT / 'docs/workshop-onboarding-20261109'
  12. RELEASE = 'ksf-workshop-onboarding-20261109-r1'
  13. sha = lambda raw: hashlib.sha256(raw).hexdigest()
  14. encode = lambda value: (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
  15. def stage():
  16. workshop_dir = ROOT / 'skills/ksf-workshop-runtime'
  17. prior = ROOT / 'docs/workshop-skill-release-20261109-r7'
  18. ready = json.loads((prior / 'release-ready.json').read_bytes())
  19. assert sha((prior / ready['zip']).read_bytes()) == ready['sha256']
  20. with zipfile.ZipFile(prior / ready['zip']) as z:
  21. r7 = json.loads(z.read('release.json'))['candidate']
  22. assert len(r7) == 43
  23. workshop = {}
  24. package = {}
  25. for name, expected in r7.items():
  26. raw = (workshop_dir / name).read_bytes()
  27. assert sha(raw) == expected, name
  28. assert not any(term in name.lower() for term in ['credential-binding', 'private.pem', 'envelope'])
  29. workshop[name] = expected
  30. package['skills/ksf-workshop-runtime/' + name] = raw
  31. source = json.loads((OUT / 'source-audit/voc-source.json').read_bytes())
  32. raw = (OUT / 'source-audit/voc-skill-0.3.42.tgz').read_bytes()
  33. assert sha(raw) == source['sha256'] and len(raw) == source['bytes']
  34. voc_files = {}
  35. with tarfile.open(fileobj=io.BytesIO(raw), mode='r:gz') as archive:
  36. for member in archive.getmembers():
  37. assert member.isfile() and member.name.startswith('package/')
  38. name = member.name[len('package/'):]
  39. assert name not in voc_files and '..' not in Path(name).parts and not name.startswith('/')
  40. voc_files[name] = sha(archive.extractfile(member).read())
  41. assert len(voc_files) == 169
  42. links = 0
  43. for name in workshop:
  44. if name.endswith('.md'):
  45. p = workshop_dir / name
  46. for target in re.findall(r'\]\(([^)]+)\)', p.read_text(encoding='utf-8')):
  47. if '://' in target or target.startswith('#'):
  48. continue
  49. resolved = (p.parent / target.split('#', 1)[0]).resolve()
  50. assert resolved.is_file() and resolved.is_relative_to(workshop_dir.resolve())
  51. links += 1
  52. meta = {'schema': 'ksf.workshop-onboarding.v1', 'releaseId': RELEASE,
  53. 'workshop': {'name': 'ksf-workshop-runtime', 'version': 'r7', 'files': workshop,
  54. 'sourceReleaseSha256': ready['sha256'], 'relativeLinks': links},
  55. 'voc': {**{k: source[k] for k in ['name', 'version', 'url', 'integrity', 'sha256', 'bytes']},
  56. 'files': voc_files, 'distribution': 'download-from-official-npm-no-third-party-artifact-included'},
  57. 'installation': {'freshInstallSupported': True, 'existingMatchingVersion': 'verify-only',
  58. 'existingDifferentVersion': 'stop-preserve-and-use-host-upgrade-path',
  59. 'hostSkillsDirectory': 'must-be-provided-by-host',
  60. 'hostConfigurationModifiedByInstaller': False},
  61. 'credentialsIncluded': False, 'databaseIncluded': False, 'remoteInstalled': False,
  62. 'doubaoNativeSupportVerified': False}
  63. package.update({'release.json': encode(meta), 'README.md': (OUT / 'README.md').read_bytes(),
  64. 'install.py': (ROOT / 'scripts/workshop-onboarding/install.py').read_bytes(),
  65. 'check-voc.cjs': (ROOT / 'scripts/workshop-onboarding/check-voc.cjs').read_bytes(),
  66. '初始化提示词.txt': (OUT / '一键初始化正文.txt').read_bytes()})
  67. validation = OUT / 'local-validation.json'
  68. if validation.exists():
  69. value = json.loads(validation.read_bytes())
  70. assert value['ok'] and value['installerSha256'] == sha(package['install.py'])
  71. assert value['runtimeCheckSha256'] == sha(package['check-voc.cjs'])
  72. package[validation.name] = validation.read_bytes()
  73. package['FILES.sha256'] = ''.join(sha(raw) + ' ' + name + '\n' for name, raw in sorted(package.items())).encode()
  74. for name, raw in package.items():
  75. p = OUT / 'package' / name
  76. p.parent.mkdir(parents=True, exist_ok=True)
  77. p.write_bytes(raw)
  78. return package, meta
  79. def main():
  80. parser = argparse.ArgumentParser()
  81. parser.add_argument('--pack', action='store_true')
  82. args = parser.parse_args()
  83. package, meta = stage()
  84. if not args.pack:
  85. print(json.dumps({'ok': True, 'staged': len(package), 'workshopFiles': 43, 'vocOriginalFiles': 169}))
  86. return
  87. assert 'local-validation.json' in package, 'Validation required before release'
  88. target = OUT / (RELEASE + '.zip')
  89. assert not target.exists(), 'Never overwrite a released archive'
  90. with zipfile.ZipFile(target, 'x', zipfile.ZIP_DEFLATED) as z:
  91. for name, raw in sorted(package.items()):
  92. info = zipfile.ZipInfo(name, (2026, 11, 9, 0, 0, 0))
  93. info.external_attr = 0o100600 << 16
  94. z.writestr(info, raw, compress_type=zipfile.ZIP_DEFLATED)
  95. with zipfile.ZipFile(target) as z:
  96. assert z.testzip() is None and set(z.namelist()) == set(package)
  97. assert all(z.read(name) == raw for name, raw in package.items())
  98. receipt = {'ok': True, 'releaseId': RELEASE, 'filename': target.name, 'bytes': target.stat().st_size,
  99. 'sha256': sha(target.read_bytes()), 'packageEntries': len(package),
  100. 'workshopFiles': 43, 'vocVersion': meta['voc']['version'], 'vocDownloadedFromOfficialSource': True,
  101. 'thirdPartyArtifactIncluded': False, 'gitPublished': False, 'remoteInstalled': False,
  102. 'doubaoActualInstallationVerified': False}
  103. (OUT / 'package-receipt.json').write_bytes(encode(receipt))
  104. print(json.dumps(receipt, ensure_ascii=False, indent=2))
  105. if __name__ == '__main__':
  106. main()