package-library-publish.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. """Freeze the Python3.9 / cross-host library channel patch from a committed revision."""
  2. import argparse
  3. import hashlib
  4. import importlib.util
  5. import io
  6. import json
  7. from pathlib import Path
  8. import shutil
  9. import subprocess
  10. import tempfile
  11. import zipfile
  12. ROOT=Path(__file__).resolve().parents[1]
  13. BASE='cd40f22a047cbaacfe458d550670d1fbff6762d3'
  14. NAME='ksf-library-publish-20261106-r1'
  15. MODULES=('voc_db.py','agent_batch.py','update_library.py','library_channel.py','library_receiver.py')
  16. DEPS=('schema.sql','taxonomy.json','retrieval.py','schema_catalog.py','intake_contract.py','structured_batch.py','product_insights.py','board_library.py')
  17. sha=lambda b:hashlib.sha256(b).hexdigest()
  18. norm=lambda b:sha(b.replace(b'\r\n',b'\n'))
  19. def main():
  20. p=argparse.ArgumentParser(description=__doc__);p.add_argument('--commit',default='HEAD');a=p.parse_args()
  21. commit=subprocess.check_output(['git','rev-parse',a.commit+'^{commit}'],cwd=ROOT).decode().strip()
  22. subprocess.run(['git','merge-base','--is-ancestor',BASE,commit],cwd=ROOT,check=True)
  23. frozen=zipfile.ZipFile(io.BytesIO(subprocess.check_output(['git','archive','--format=zip',commit],cwd=ROOT)))
  24. old=zipfile.ZipFile(io.BytesIO(subprocess.check_output(['git','archive','--format=zip',BASE],cwd=ROOT)))
  25. out=ROOT/'artifacts/跨主机入库与兼容修复-20261106';package=ROOT/'work'/NAME/'package'
  26. if package.exists() or (out/(NAME+'.zip')).exists():raise ValueError('RELEASE_ALREADY_EXISTS')
  27. package.mkdir(parents=True)
  28. def write(n,b):
  29. q=package/n;q.parent.mkdir(parents=True,exist_ok=True);q.write_bytes(b)
  30. def js(n,v):write(n,(json.dumps(v,ensure_ascii=False,indent=2)+'\n').encode())
  31. def entry(n):return dict(path=n,sha256=sha(frozen.read(n)),priorNormalizedSha256=[norm(old.read(n))] if n in old.namelist() else [],allowMissing=n not in old.namelist())
  32. for n in MODULES:write('payload/server/voc-library/'+n,frozen.read('server/voc-library/'+n))
  33. for n in frozen.namelist():
  34. if n.startswith('skills/') and not n.endswith('/'):write(n,frozen.read(n))
  35. for n in ('ksfGetDataDictionary.js','manifest.json'):write('functions/'+n,(ROOT/'server/dist/data-dictionary'/n).read_bytes())
  36. for n in ('server/src/postgres.mjs','server/src/core.mjs','server/src/data-dictionary.mjs','server/references/runtime-data.json','server/references/business-data-dictionary.json'):
  37. assert norm((ROOT/n).read_bytes())==norm(frozen.read(n)),'FUNCTION_SOURCE_NOT_FROZEN'
  38. function=json.loads((package/'functions/manifest.json').read_text(encoding='utf-8'))
  39. assert function['functions'][0]['name']=='ksfGetDataDictionary' and len(function['functions'])==1
  40. for item in function['functions']:assert sha((package/'functions'/item['file']).read_bytes())==item['sha256']
  41. function['functions'][0].update(objectId='Z41ABwyxHO',beforeSha256='e53f148c4ca34085c176f595c2f402c107ce95bcde6107840932cc400d2fe08e',operation='update-code-only')
  42. function['existingFunctionsModified']=True;function['scope']='only ksfGetDataDictionary code; preserve ACL/schema/other functions'
  43. js('functions/manifest.json',function)
  44. js('deploy/backend-files.json',{'files':[entry('server/voc-library/'+n) for n in MODULES]})
  45. js('deploy/dependencies.json',{'files':[dict(path='server/voc-library/'+n,normalizedSha256=norm(frozen.read('server/voc-library/'+n))) for n in DEPS]})
  46. for n in ('发给运维001.md','发给王刚001.md','实现与验证.md'):write(n,frozen.read('docs/library-publish-20261106/'+n))
  47. write('install-query-skills.py',frozen.read('scripts/install-query-skills.py'))
  48. for n in ('install-voc-database.py','verify-installed-voc-database.py','reassemble-voc-database.py'):
  49. write('utilities/'+n,frozen.read('scripts/'+n))
  50. baselines={name:[{n.split('/',2)[2]:sha(old.read(n)) for n in old.namelist() if n.startswith('skills/'+name+'/') and not n.endswith('/')}]
  51. for name in ('ksf-workshop-runtime','ksf-voc-library')}
  52. js('baselines.json',baselines)
  53. js('examples/channel.json',{'schema':'ksf.library-channel.v1','host':'172.0.0.100','port':22,'user':'ksf-library-publisher',
  54. 'identityFile':'/opt/data/ksf-library-channel/id_ed25519','knownHostsFile':'/opt/data/ksf-library-channel/known_hosts'})
  55. js('examples/receiver.json',{'schema':'ksf.library-channel.v1','registry':'/opt/ksf-voc-library/registry','intake':'/opt/ksf-voc-intake','canPublish':True})
  56. spec=importlib.util.spec_from_file_location('channel_skill_installer',package/'install-query-skills.py');installer=importlib.util.module_from_spec(spec);spec.loader.exec_module(installer)
  57. skills={}
  58. for name in baselines:
  59. folder=package/'skills'/name
  60. for line in (folder/'FILES.sha256').read_text(encoding='utf-8').splitlines():
  61. h,n=line.split(' ',1);assert sha((folder/n).read_bytes())==h
  62. skills[name]={'entries':len((folder/'FILES.sha256').read_text().splitlines()),'manifestSha256':sha((folder/'FILES.sha256').read_bytes())}
  63. with tempfile.TemporaryDirectory(prefix='install-check-',dir=package.parent) as td:
  64. data=Path(td)
  65. for name,versions in baselines.items():
  66. for n in versions[0]:
  67. target=data/'skills'/name/n;target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(old.read('skills/'+name+'/'+n))
  68. binding=data/'skills/ksf-workshop-runtime/credential-binding.json';binding.write_text('preserved')
  69. session=data/'ksf-workshop-state/sessions/existing.json';session.parent.mkdir(parents=True);session.write_text('preserved')
  70. plan=installer.install(package,data);assert plan['filesChecked']==34
  71. assert installer.install(package,data,True)['filesystemVerified'];assert not installer.install(package,data,True)['applied']
  72. assert binding.read_text()==session.read_text()=='preserved'
  73. js('RELEASE.json',dict(release=NAME,sourceCommit=commit,baselineCommit=BASE,skills=skills,installerCheck=plan,
  74. installerApplyVerified=True,installerNoOpVerified=True,backendFiles=len(MODULES),staticFiles=0,
  75. cloudFunctions=function['functions'],businessDeclaredFields=1388,businessPhysicalSnapshotFields=1451,
  76. pythonMinimum='3.9',databaseIncluded=False,credentialsIncluded=False,productionInstalled=False,
  77. sshChannelConfigured=False,productionIngestionVerified=False))
  78. files=sorted(q for q in package.rglob('*') if q.is_file())
  79. assert not any(q.suffix in ('.sqlite','.db','.key','.pyc') or q.name.startswith('.env') for q in files)
  80. write('FILES.sha256',''.join(sha(q.read_bytes())+' '+q.relative_to(package).as_posix()+'\n' for q in files).encode())
  81. out.mkdir(parents=True,exist_ok=True);archive=out/(NAME+'.zip')
  82. with zipfile.ZipFile(archive,'x',zipfile.ZIP_DEFLATED,compresslevel=9) as z:
  83. for q in [*files,package/'FILES.sha256']:z.write(q,q.relative_to(package).as_posix())
  84. with zipfile.ZipFile(archive) as z:
  85. assert z.testzip() is None
  86. for line in z.read('FILES.sha256').decode().splitlines():
  87. h,n=line.split(' ',1);assert sha(z.read(n))==h
  88. receipt=dict(ok=True,filename=archive.name,bytes=archive.stat().st_size,sha256=sha(archive.read_bytes()),entries=len(files)+1,
  89. filesVerified=len(files),sourceCommit=commit,productionInstalled=False)
  90. (out/'delivery-receipt.json').write_text(json.dumps(receipt,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
  91. for n in ('发给运维001.md','发给王刚001.md','实现与验证.md'):shutil.copyfile(package/n,out/n)
  92. print(json.dumps(receipt,ensure_ascii=False,indent=2))
  93. if __name__=='__main__':main()