publish-workshop-baseline-upgrade.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. """Publish the strict known-baseline platform upgrade; target Skill remains the already-published r10."""
  2. import argparse
  3. import hashlib
  4. import io
  5. import json
  6. from pathlib import Path
  7. import re
  8. import shutil
  9. import subprocess
  10. import urllib.request
  11. import zipfile
  12. ROOT = Path(__file__).resolve().parents[1]
  13. OUT = ROOT / 'docs/ksf-workshop-r10-baseline-upgrade-20261110-r1'
  14. STAGING = ROOT / 'work/workshop-r10-baseline-upgrade-20261110-r1-publication'
  15. BRANCH = 'codex/workshop-r10-baseline-upgrade-20261110-r1'
  16. REMOTE = 'https://git.fmode.cn/18307996893/kangshifu-board.git'
  17. PUBLIC = REMOTE[:-4]
  18. sha = lambda value: hashlib.sha256(value).hexdigest()
  19. encode = lambda value: (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
  20. def need(ok, code):
  21. if not ok:
  22. raise ValueError(code)
  23. def git(*args, cwd=STAGING, data=None):
  24. result = subprocess.run(['git', *args], cwd=cwd, input=data, capture_output=True, timeout=90)
  25. need(result.returncode == 0, 'GIT_OPERATION_FAILED')
  26. return result.stdout.decode('utf-8').strip()
  27. def secret_scan(files):
  28. # Compare actual private values in memory only; never include them or their fingerprints in output.
  29. private = Path('F:/文档/正式环境连接配置.txt').read_text('utf-8-sig')
  30. match = re.search(r'^\|\s*masterKey\s*\|\s*`?([^|`\s]+)', private, re.M)
  31. need(match is not None, 'LOCAL_SECRET_CHECK_CONFIG_MISSING')
  32. secrets = [match.group(1).encode('utf-8')]
  33. env = Path('F:/文档/康师傅数字人受限连接.env').read_text('utf-8-sig')
  34. for key, value in re.findall(r'^([A-Z0-9_]+)=(.+)$', env, re.M):
  35. if re.search('TOKEN|SECRET|PASSWORD|MASTER_KEY', key):
  36. value = value.strip().strip('\"\'')
  37. if len(value) >= 16:
  38. secrets.append(value.encode('utf-8'))
  39. checked = 0
  40. for name, raw in files.items():
  41. bodies = [(name, raw)]
  42. if name.endswith('.zip'):
  43. with zipfile.ZipFile(io.BytesIO(raw)) as z:
  44. bodies.extend((name + ':' + n, z.read(n)) for n in z.namelist())
  45. for label, body in bodies:
  46. need(not any(value in body for value in secrets), 'SECRET_VALUE_DETECTED')
  47. need(b'-----BEGIN PRIVATE KEY-----' not in body and b'-----BEGIN RSA PRIVATE KEY-----' not in body,
  48. 'PRIVATE_KEY_DETECTED')
  49. checked += 1
  50. return {'ok': True, 'filesAndArchiveMembersChecked': checked, 'credentialValuesIncluded': False}
  51. def prepare():
  52. need(not STAGING.exists(), 'NEW_PUBLICATION_DIRECTORY_REQUIRED')
  53. distribution = json.loads((OUT / 'publish/distribution.json').read_bytes())
  54. archive = distribution['archive']['path']
  55. names = ['README.md', 'distribution.json', archive]
  56. files = {n: (OUT / 'publish' / n).read_bytes() for n in names}
  57. need(sha(files[archive]) == distribution['archive']['sha256'], 'ARCHIVE_CHANGED')
  58. for p in (OUT / 'package').rglob('*'):
  59. if p.is_file():
  60. files['source/' + p.relative_to(OUT / 'package').as_posix()] = p.read_bytes()
  61. files['.gitattributes'] = b'* -text\n'
  62. scan = secret_scan(files)
  63. (OUT / 'public-secret-check.json').write_bytes(encode(scan))
  64. STAGING.mkdir(parents=True)
  65. for name, raw in files.items():
  66. p = STAGING / name
  67. p.parent.mkdir(parents=True, exist_ok=True)
  68. p.write_bytes(raw)
  69. git('init', '--initial-branch=' + BRANCH)
  70. git('config', 'user.name', git('config', '--get', 'user.name', cwd=ROOT))
  71. git('config', 'user.email', git('config', '--get', 'user.email', cwd=ROOT))
  72. git('config', 'core.autocrlf', 'false')
  73. git('remote', 'add', 'origin', REMOTE)
  74. git('add', '--', *sorted(files))
  75. git('commit', '-m', 'release: strict r6 baseline and missing-document support for unchanged workshop Skill r10')
  76. need(set(git('ls-files').splitlines()) == set(files), 'UNEXPECTED_PUBLISHED_FILES')
  77. for name, raw in files.items():
  78. oid = git('rev-parse', 'HEAD:' + name)
  79. result = subprocess.run(['git', 'cat-file', 'blob', oid], cwd=STAGING, capture_output=True, check=True)
  80. need(result.stdout == raw, 'GIT_BLOB_CHANGED')
  81. commit = git('rev-parse', 'HEAD')
  82. plan = {'ok': True, 'commit': commit, 'branch': BRANCH, 'files': {n: sha(raw) for n, raw in files.items()},
  83. 'published': False, 'mainBranchModified': False, 'secretCheck': scan}
  84. (OUT / 'git-plan.json').write_bytes(encode(plan))
  85. print(json.dumps({'ok': True, 'prepared': True, 'commit': commit, 'files': len(files)}))
  86. def publish():
  87. plan = json.loads((OUT / 'git-plan.json').read_bytes())
  88. commit = plan['commit']
  89. need(git('rev-parse', 'HEAD') == commit and not git('status', '--porcelain'), 'PUBLICATION_STAGING_CHANGED')
  90. remote = git('ls-remote', 'origin', 'refs/heads/' + BRANCH)
  91. need(not remote or remote.split()[0] == commit, 'REMOTE_BRANCH_ALREADY_DIFFERS')
  92. git('push', 'origin', 'HEAD:refs/heads/' + BRANCH)
  93. need(git('ls-remote', 'origin', 'refs/heads/' + BRANCH).split()[0] == commit, 'PUSH_NOT_CONFIRMED')
  94. checks = []
  95. # Anonymous reads, without platform credentials, verify all public artifacts and sources.
  96. for name, expected in plan['files'].items():
  97. url = PUBLIC + '/raw/' + commit + '/' + name
  98. request = urllib.request.Request(url, headers={'User-Agent': 'ksf-release-verifier/1'})
  99. with urllib.request.urlopen(request, timeout=40) as response:
  100. raw = response.read()
  101. need(response.status == 200 and sha(raw) == expected, 'ONLINE_FILE_HASH_MISMATCH')
  102. checks.append({'path': name, 'url': url, 'status': response.status, 'bytes': len(raw),
  103. 'sha256': sha(raw), 'matchesLocal': True})
  104. guide = PUBLIC + '/raw/' + commit + '/README.md'
  105. prompt = ('请完成原工作坊Agent的r10升级。固定执行单:\n' + guide + '\n'
  106. '核同目录distribution.json及ZIP后,按执行单完成plan/apply/verify。'
  107. '只接受已知完整历史基线;若不匹配,停止并回传inventory的公开文件指纹,不绕过保护。'
  108. '保留身份、授权、连接及7个运行文件,不安装VOC、不写业务记录、不重跑迁移。\n')
  109. (OUT / '执行正文.txt').write_text(prompt, encoding='utf-8')
  110. receipt = {'ok': True, 'published': True, 'releaseId': 'ksf-workshop-r10-baseline-upgrade-20261110-r1',
  111. 'repository': PUBLIC, 'branch': BRANCH, 'commit': commit, 'guideUrl': guide,
  112. 'resources': checks, 'allPublicFilesReadBack': True, 'mainBranchModified': False,
  113. 'workshopSkillVersion': 'r10', 'supportedUpgradeFrom': ['r6', 'r7', 'r8', 'r9'],
  114. 'productionDatabaseCutoverPreviouslyConfirmed': True, 'productionMigrationExecutedByPublisher': False,
  115. 'targetHostInstallationVerified': False, 'businessDataModified': False}
  116. (OUT / 'online-release-receipt.json').write_bytes(encode(receipt))
  117. print(json.dumps({'ok': True, 'published': True, 'commit': commit, 'guideUrl': guide,
  118. 'publicFilesVerified': len(checks), 'productionMigrationExecuted': False}))
  119. if __name__ == '__main__':
  120. p = argparse.ArgumentParser(description=__doc__)
  121. p.add_argument('command', choices=('prepare', 'publish'))
  122. args = p.parse_args()
  123. prepare() if args.command == 'prepare' else publish()