prepare-board-deployment.py 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. """Freeze the pushed board commit; never package uncommitted code or private data."""
  2. import concurrent.futures
  3. import hashlib
  4. import io
  5. import json
  6. from pathlib import Path
  7. import subprocess
  8. import urllib.request
  9. import zipfile
  10. ROOT = Path(__file__).resolve().parents[1]
  11. COMMIT = '0ba3c0a07afb70aed54713739d3c128dacc1aaf0'
  12. ORIGIN = 'https://kangshifu.fmode.cn'
  13. OUT = ROOT / 'work/board-deploy-20261106-r1'
  14. PKG = OUT / 'package'
  15. sha = lambda b: hashlib.sha256(b).hexdigest()
  16. norm = lambda b: sha(b.replace(b'\r\n', b'\n'))
  17. def remote(p):
  18. req = urllib.request.Request(ORIGIN + '/' + p, headers={'Cache-Control': 'no-cache'})
  19. with urllib.request.urlopen(req, timeout=35) as r:
  20. return r.read(), r.headers.get('content-type', '')
  21. def prepare():
  22. if PKG.exists():
  23. raise RuntimeError('PACKAGE_ALREADY_EXISTS')
  24. source = zipfile.ZipFile(io.BytesIO(subprocess.check_output(['git', 'archive', '--format=zip', COMMIT], cwd=ROOT)))
  25. members = [p for p in source.namelist() if not p.endswith('/') and (
  26. (p.startswith('src/') and not p.startswith('src/dev/')) or
  27. p.startswith(('server/src/', 'server/references/', 'server/web/', 'server/test/', 'test/')) or
  28. p in {'package.json', 'server/scripts/build-member-groups.mjs', 'server/library-api.mjs'} or
  29. (p.startswith('server/voc-library/') and not Path(p).name.startswith('test_')))]
  30. PKG.mkdir(parents=True)
  31. for p in members:
  32. target = PKG / 'payload' / p
  33. target.parent.mkdir(parents=True, exist_ok=True)
  34. target.write_bytes(source.read(p))
  35. front = sorted(p for p in members if p.startswith('src/'))
  36. index, _ = remote('index.html')
  37. production = dict(zip(front, concurrent.futures.ThreadPoolExecutor(max_workers=6).map(remote, front)))
  38. files = []
  39. unknown = []
  40. for p in front:
  41. body, kind = production[p]
  42. after = source.read(p)
  43. # SPA fallback is an absent module, never a successfully deployed JavaScript file.
  44. absent = body == index and 'text/html' in kind
  45. baseline = [] if absent else [norm(body)]
  46. known = absent or norm(body) == norm(after)
  47. if not known:
  48. for revision in ('bb38e3732e8ae102ff98345abfee5c1caba13d71', 'd14d02f', '7d01b5c'):
  49. before = subprocess.run(['git', 'show', revision + ':' + p], cwd=ROOT, capture_output=True)
  50. if before.returncode == 0 and norm(before.stdout) == norm(body):
  51. known = True
  52. break
  53. if not known:
  54. unknown.append(p)
  55. target = OUT / 'production-before' / p
  56. target.parent.mkdir(parents=True, exist_ok=True)
  57. target.write_bytes(body)
  58. files.append({'path': p, 'sha256': sha(after), 'priorNormalizedSha256': baseline,
  59. 'allowMissing': absent, 'observedState': 'spa-fallback' if absent else 'static-file',
  60. 'observedSha256': sha(body), 'changed': absent or norm(body) != norm(after),
  61. 'matchesKnownGitVersion': known})
  62. manifest = {'version': 'ksf.board.publish.v1', 'release': 'ksf.board.20261106.r1',
  63. 'commit': COMMIT, 'origin': ORIGIN, 'homepageSha256': sha(index), 'files': files}
  64. (PKG / 'deploy').mkdir()
  65. (PKG / 'deploy/board-files.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  66. receipt = {'commit': COMMIT, 'staticFiles': len(files), 'changedFiles': sum(x['changed'] for x in files),
  67. 'newFiles': sum(x['allowMissing'] for x in files), 'unknownProductionBaselines': unknown,
  68. 'homepageSha256': sha(index), 'productionWrites': 0, 'productionBrowserVerified': False}
  69. (OUT / 'preparation.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2), encoding='utf-8')
  70. print(json.dumps(receipt, ensure_ascii=False))
  71. if __name__ == '__main__':
  72. prepare()