| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- """Freeze the Python3.9 / cross-host library channel patch from a committed revision."""
- import argparse
- import hashlib
- import importlib.util
- import io
- import json
- from pathlib import Path
- import shutil
- import subprocess
- import tempfile
- import zipfile
- ROOT=Path(__file__).resolve().parents[1]
- BASE='cd40f22a047cbaacfe458d550670d1fbff6762d3'
- NAME='ksf-library-publish-20261106-r1'
- MODULES=('voc_db.py','agent_batch.py','update_library.py','library_channel.py','library_receiver.py')
- DEPS=('schema.sql','taxonomy.json','retrieval.py','schema_catalog.py','intake_contract.py','structured_batch.py','product_insights.py','board_library.py')
- sha=lambda b:hashlib.sha256(b).hexdigest()
- norm=lambda b:sha(b.replace(b'\r\n',b'\n'))
- def main():
- p=argparse.ArgumentParser(description=__doc__);p.add_argument('--commit',default='HEAD');a=p.parse_args()
- commit=subprocess.check_output(['git','rev-parse',a.commit+'^{commit}'],cwd=ROOT).decode().strip()
- subprocess.run(['git','merge-base','--is-ancestor',BASE,commit],cwd=ROOT,check=True)
- frozen=zipfile.ZipFile(io.BytesIO(subprocess.check_output(['git','archive','--format=zip',commit],cwd=ROOT)))
- old=zipfile.ZipFile(io.BytesIO(subprocess.check_output(['git','archive','--format=zip',BASE],cwd=ROOT)))
- out=ROOT/'artifacts/跨主机入库与兼容修复-20261106';package=ROOT/'work'/NAME/'package'
- if package.exists() or (out/(NAME+'.zip')).exists():raise ValueError('RELEASE_ALREADY_EXISTS')
- package.mkdir(parents=True)
- def write(n,b):
- q=package/n;q.parent.mkdir(parents=True,exist_ok=True);q.write_bytes(b)
- def js(n,v):write(n,(json.dumps(v,ensure_ascii=False,indent=2)+'\n').encode())
- 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())
- for n in MODULES:write('payload/server/voc-library/'+n,frozen.read('server/voc-library/'+n))
- for n in frozen.namelist():
- if n.startswith('skills/') and not n.endswith('/'):write(n,frozen.read(n))
- for n in ('ksfGetDataDictionary.js','manifest.json'):write('functions/'+n,(ROOT/'server/dist/data-dictionary'/n).read_bytes())
- 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'):
- assert norm((ROOT/n).read_bytes())==norm(frozen.read(n)),'FUNCTION_SOURCE_NOT_FROZEN'
- function=json.loads((package/'functions/manifest.json').read_text(encoding='utf-8'))
- assert function['functions'][0]['name']=='ksfGetDataDictionary' and len(function['functions'])==1
- for item in function['functions']:assert sha((package/'functions'/item['file']).read_bytes())==item['sha256']
- function['functions'][0].update(objectId='Z41ABwyxHO',beforeSha256='e53f148c4ca34085c176f595c2f402c107ce95bcde6107840932cc400d2fe08e',operation='update-code-only')
- function['existingFunctionsModified']=True;function['scope']='only ksfGetDataDictionary code; preserve ACL/schema/other functions'
- js('functions/manifest.json',function)
- js('deploy/backend-files.json',{'files':[entry('server/voc-library/'+n) for n in MODULES]})
- js('deploy/dependencies.json',{'files':[dict(path='server/voc-library/'+n,normalizedSha256=norm(frozen.read('server/voc-library/'+n))) for n in DEPS]})
- for n in ('发给运维001.md','发给王刚001.md','实现与验证.md'):write(n,frozen.read('docs/library-publish-20261106/'+n))
- write('install-query-skills.py',frozen.read('scripts/install-query-skills.py'))
- for n in ('install-voc-database.py','verify-installed-voc-database.py','reassemble-voc-database.py'):
- write('utilities/'+n,frozen.read('scripts/'+n))
- baselines={name:[{n.split('/',2)[2]:sha(old.read(n)) for n in old.namelist() if n.startswith('skills/'+name+'/') and not n.endswith('/')}]
- for name in ('ksf-workshop-runtime','ksf-voc-library')}
- js('baselines.json',baselines)
- js('examples/channel.json',{'schema':'ksf.library-channel.v1','host':'172.0.0.100','port':22,'user':'ksf-library-publisher',
- 'identityFile':'/opt/data/ksf-library-channel/id_ed25519','knownHostsFile':'/opt/data/ksf-library-channel/known_hosts'})
- js('examples/receiver.json',{'schema':'ksf.library-channel.v1','registry':'/opt/ksf-voc-library/registry','intake':'/opt/ksf-voc-intake','canPublish':True})
- 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)
- skills={}
- for name in baselines:
- folder=package/'skills'/name
- for line in (folder/'FILES.sha256').read_text(encoding='utf-8').splitlines():
- h,n=line.split(' ',1);assert sha((folder/n).read_bytes())==h
- skills[name]={'entries':len((folder/'FILES.sha256').read_text().splitlines()),'manifestSha256':sha((folder/'FILES.sha256').read_bytes())}
- with tempfile.TemporaryDirectory(prefix='install-check-',dir=package.parent) as td:
- data=Path(td)
- for name,versions in baselines.items():
- for n in versions[0]:
- target=data/'skills'/name/n;target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(old.read('skills/'+name+'/'+n))
- binding=data/'skills/ksf-workshop-runtime/credential-binding.json';binding.write_text('preserved')
- session=data/'ksf-workshop-state/sessions/existing.json';session.parent.mkdir(parents=True);session.write_text('preserved')
- plan=installer.install(package,data);assert plan['filesChecked']==34
- assert installer.install(package,data,True)['filesystemVerified'];assert not installer.install(package,data,True)['applied']
- assert binding.read_text()==session.read_text()=='preserved'
- js('RELEASE.json',dict(release=NAME,sourceCommit=commit,baselineCommit=BASE,skills=skills,installerCheck=plan,
- installerApplyVerified=True,installerNoOpVerified=True,backendFiles=len(MODULES),staticFiles=0,
- cloudFunctions=function['functions'],businessDeclaredFields=1388,businessPhysicalSnapshotFields=1451,
- pythonMinimum='3.9',databaseIncluded=False,credentialsIncluded=False,productionInstalled=False,
- sshChannelConfigured=False,productionIngestionVerified=False))
- files=sorted(q for q in package.rglob('*') if q.is_file())
- assert not any(q.suffix in ('.sqlite','.db','.key','.pyc') or q.name.startswith('.env') for q in files)
- write('FILES.sha256',''.join(sha(q.read_bytes())+' '+q.relative_to(package).as_posix()+'\n' for q in files).encode())
- out.mkdir(parents=True,exist_ok=True);archive=out/(NAME+'.zip')
- with zipfile.ZipFile(archive,'x',zipfile.ZIP_DEFLATED,compresslevel=9) as z:
- for q in [*files,package/'FILES.sha256']:z.write(q,q.relative_to(package).as_posix())
- with zipfile.ZipFile(archive) as z:
- assert z.testzip() is None
- for line in z.read('FILES.sha256').decode().splitlines():
- h,n=line.split(' ',1);assert sha(z.read(n))==h
- receipt=dict(ok=True,filename=archive.name,bytes=archive.stat().st_size,sha256=sha(archive.read_bytes()),entries=len(files)+1,
- filesVerified=len(files),sourceCommit=commit,productionInstalled=False)
- (out/'delivery-receipt.json').write_text(json.dumps(receipt,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
- for n in ('发给运维001.md','发给王刚001.md','实现与验证.md'):shutil.copyfile(package/n,out/n)
- print(json.dumps(receipt,ensure_ascii=False,indent=2))
- if __name__=='__main__':main()
|