release-unified-runtime-hotfix.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. """Seal and publish the runtime repair separately from migration r3 and workshop r9."""
  2. import argparse
  3. import ast
  4. import difflib
  5. import hashlib
  6. import importlib.util
  7. import io
  8. import json
  9. from pathlib import Path
  10. import re
  11. import subprocess
  12. import urllib.request
  13. import zipfile
  14. ROOT=Path(__file__).resolve().parents[1]
  15. NAME='ksf-unified-runtime-hotfix-20261109-r1'
  16. OUT=ROOT/'docs'/NAME
  17. TEST=ROOT/'work/unified-runtime-hotfix-20261109'
  18. STAGING=ROOT/'work'/('publish-'+NAME)
  19. BASE=ROOT/'docs/workshop-onboarding-20261109-r4/publish/operations/ksf-unified-database-migration-20261109-r3.zip'
  20. BASE_SHA='6bf273509ea6f016a5b3e6c38d077bbce158e677bd701e189c80e016788d93c2'
  21. BRANCH='codex/unified-runtime-hotfix-20261109-r1'
  22. REMOTE='https://git.fmode.cn/18307996893/kangshifu-board.git'
  23. PUBLIC=REMOTE[:-4]
  24. sha=lambda raw:hashlib.sha256(raw).hexdigest()
  25. encode=lambda obj:(json.dumps(obj,ensure_ascii=False,indent=2)+'\n').encode('utf-8')
  26. def need(ok,code):
  27. if not ok:raise ValueError(code)
  28. def git(*args,cwd=STAGING):
  29. p=subprocess.run(['git',*args],cwd=cwd,capture_output=True,timeout=120)
  30. need(p.returncode==0,'GIT_OPERATION_FAILED')
  31. return p.stdout.decode('utf-8').strip()
  32. def prepare():
  33. need(not OUT.exists() and not STAGING.exists(),'NEW_RELEASE_DIRECTORY_REQUIRED')
  34. need(sha(BASE.read_bytes())==BASE_SHA,'SEALED_R3_CHANGED')
  35. validation=json.loads((TEST/'validation.json').read_bytes())
  36. need(validation['ok'] and validation['realPostgreSQL'],'LOCAL_PG_VALIDATION_REQUIRED')
  37. for name in ('targeted-regression','real-pg'):
  38. raw=(TEST/(name+'.log')).read_text('utf-8')
  39. need(re.search(r'\nOK\s*$',raw),'LOCAL_TEST_FAILURE')
  40. real=json.loads((TEST/'real-final2/real-archive-validation.json').read_bytes())
  41. receiver=json.loads((TEST/'real-final2/real-receiver-validation.json').read_bytes())
  42. need(real['ok'] and receiver['ok'],'REAL_ARCHIVE_VALIDATION_REQUIRED')
  43. with zipfile.ZipFile(BASE) as z:
  44. baseline={n:z.read(n) for n in z.namelist()}
  45. payload={n:raw for n,raw in baseline.items() if n.startswith(('server/','scripts/','components/research-runtime/'))
  46. and n!='scripts/migrate-unified-database.py'}
  47. patches=[];changes=[]
  48. for module in ('retrieval.py','update_library.py'):
  49. n='server/voc-library/'+module;raw=(ROOT/n).read_bytes()
  50. need(raw!=baseline[n],'EMPTY_REPAIR')
  51. payload[n]=raw
  52. payload['components/research-runtime/scripts/'+module]=raw
  53. changes.append({'path':n,'sealedR3Sha256':sha(baseline[n]),'afterSha256':sha(raw)})
  54. patches.extend(difflib.unified_diff(baseline[n].decode('utf-8').splitlines(True),raw.decode('utf-8').splitlines(True),
  55. fromfile='sealed-r3/'+n,tofile=NAME+'/'+n))
  56. component='components/research-runtime/'
  57. payload[component+'FILES.sha256']=''.join(sha(raw)+' '+n[len(component):]+'\n'
  58. for n,raw in sorted(payload.items()) if n.startswith(component) and n!=component+'FILES.sha256').encode()
  59. for n,raw in payload.items():
  60. if n.endswith('.py'):ast.parse(raw.decode('utf-8-sig'),filename=n,feature_version=(3,9))
  61. evidence={'ok':True,'targetedRegressionTests':66,'realPostgreSQLTests':15,
  62. 'genuineArchiveValidation':real,'genuineReceiverValidation':receiver,
  63. 'productionWrites':0,'productionAcceptancePending':True}
  64. payload['validation.json']=encode(evidence)
  65. for n in ('targeted-regression.log','real-pg.log'):payload['evidence/'+n]=(TEST/n).read_bytes()
  66. payload['patches/runtime.diff']=''.join(patches).encode('utf-8')
  67. readme=(ROOT/'docs/unified-runtime-hotfix-instructions-20261109.md').read_bytes()
  68. payload['README.md']=readme
  69. release={'release':NAME,'sealedMigrationBase':BASE_SHA,'database':'FKieRbIoGt_master','role':'FKieRbIoGt_admin',
  70. 'appId':'FKieRbIoGt_main','changedServerModules':changes,'serverModulesOtherThanTheseUnchanged':True,
  71. 'mainSkillIncluded':False,'manualIncluded':False,'migrationApplyToolIncluded':False,
  72. 'credentialsIncluded':False,'rawResearchDataIncluded':False,'productionDeployed':False}
  73. payload['release.json']=encode(release)
  74. payload['FILES.sha256']=''.join(sha(raw)+' '+n+'\n' for n,raw in sorted(payload.items())).encode()
  75. stream=io.BytesIO()
  76. with zipfile.ZipFile(stream,'w',zipfile.ZIP_DEFLATED) as z:
  77. for n,raw in sorted(payload.items()):z.writestr(n,raw)
  78. archive=stream.getvalue()
  79. with zipfile.ZipFile(io.BytesIO(archive)) as z:
  80. need(z.testzip() is None,'ARCHIVE_CORRUPT')
  81. for line in z.read('FILES.sha256').decode().splitlines():
  82. h,n=line.split(' ',1);need(sha(z.read(n))==h,'ARCHIVE_HASH_MISMATCH')
  83. manifest={'release':NAME,'archive':NAME+'.zip','bytes':len(archive),'sha256':sha(archive),
  84. 'filesVerified':len(payload)-1,'validation':{'targetedRegressionTests':66,'realPostgreSQLTests':15,
  85. 'realArchiveRebuildPassed':True,'realReceiverPreflightPassed':True,'productionWrites':0},
  86. 'productionDeploymentPending':True,'mainSkillAndManualDeferred':True}
  87. files={'README.md':readme,'release.json':encode(manifest),NAME+'.zip':archive,
  88. 'validation.json':encode(evidence),'patches/runtime.diff':payload['patches/runtime.diff'],'.gitattributes':b'* -text\n'}
  89. spec=importlib.util.spec_from_file_location('publication_helpers',ROOT/'scripts/publish-workshop-onboarding-r4.py')
  90. helpers=importlib.util.module_from_spec(spec);spec.loader.exec_module(helpers)
  91. scan=helpers.secret_scan(files)
  92. OUT.mkdir(parents=True);STAGING.mkdir(parents=True)
  93. for n,raw in files.items():
  94. for base in (STAGING,OUT/'publish'):
  95. p=base/n;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(raw)
  96. git('init','--initial-branch='+BRANCH)
  97. for k in ('user.name','user.email'):git('config',k,git('config','--get',k,cwd=ROOT))
  98. git('config','core.autocrlf','false');git('remote','add','origin',REMOTE)
  99. git('add','--',*sorted(files));git('commit','-m','fix: valid default facets and source locator reuse on archive rebuild')
  100. need(set(git('ls-files').splitlines())==set(files),'UNEXPECTED_RELEASE_FILES')
  101. for n,raw in files.items():
  102. body=subprocess.run(['git','show','HEAD:'+n],cwd=STAGING,capture_output=True,check=True).stdout
  103. need(body==raw,'PUBLICATION_BYTES_CHANGED')
  104. plan={'commit':git('rev-parse','HEAD'),'branch':BRANCH,'files':{n:sha(raw) for n,raw in files.items()},
  105. 'secretScan':scan,'release':manifest,'published':False}
  106. (OUT/'publication-plan.json').write_bytes(encode(plan))
  107. print(json.dumps({'ok':True,'prepared':True,'commit':plan['commit'],'archive':manifest}))
  108. def publish():
  109. plan=json.loads((OUT/'publication-plan.json').read_bytes());commit=plan['commit']
  110. need(git('rev-parse','HEAD')==commit and not git('status','--porcelain'),'RELEASE_STAGING_CHANGED')
  111. prior=git('ls-remote','origin','refs/heads/'+BRANCH)
  112. need(not prior or prior.split()[0]==commit,'REMOTE_BRANCH_DIFFERS')
  113. git('push','origin','HEAD:refs/heads/'+BRANCH)
  114. need(git('ls-remote','origin','refs/heads/'+BRANCH).split()[0]==commit,'PUSH_UNCONFIRMED')
  115. checks=[]
  116. for n,h in plan['files'].items():
  117. url=PUBLIC+'/raw/'+commit+'/'+n
  118. with urllib.request.urlopen(urllib.request.Request(url,headers={'User-Agent':'ksf-runtime-release-verifier/1'}),timeout=50) as response:
  119. raw=response.read();need(response.status==200 and sha(raw)==h,'PUBLIC_READBACK_MISMATCH')
  120. checks.append({'path':n,'url':url,'httpStatus':response.status,'bytes':len(raw),'sha256':h})
  121. receipt={'ok':True,'published':True,'commit':commit,'branch':BRANCH,'mainBranchModified':False,
  122. 'resources':checks,'release':plan['release'],'productionDeployed':False}
  123. (OUT/'online-release-receipt.json').write_bytes(encode(receipt))
  124. print(json.dumps({'ok':True,'commit':commit,'publicFilesVerified':len(checks),'readmeUrl':PUBLIC+'/raw/'+commit+'/README.md'}))
  125. if __name__=='__main__':
  126. p=argparse.ArgumentParser(description=__doc__);p.add_argument('command',choices=('prepare','publish'));a=p.parse_args()
  127. prepare() if a.command=='prepare' else publish()