| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- """Package the bounded, read-only development reference UI on the actual public baseline."""
- from concurrent.futures import ThreadPoolExecutor
- from datetime import datetime, timezone
- import difflib
- import hashlib
- import json
- from pathlib import Path
- import re
- from urllib.request import Request, urlopen
- from urllib.error import HTTPError
- import zipfile
- ROOT = Path(__file__).resolve().parents[1]
- RELEASE = 'ksf-development-references-20261108-r15'
- OUT = ROOT / 'artifacts' / RELEASE
- PACK = OUT / 'package'
- WORK = ROOT / 'work' / RELEASE
- HOME = 'dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3'
- FILES = ['src/core/industry-navigation.js', 'src/core/development-references.js',
- 'src/services/development-service.js', 'src/pages/industry-development-view.js',
- 'src/pages/view-industry.js', 'src/data/development-references.json']
- def sha(raw):
- return hashlib.sha256(raw).hexdigest()
- def normal(raw):
- return raw.replace(b'\r\n', b'\n')
- def save(name, value):
- path = PACK / name
- path.parent.mkdir(parents=True, exist_ok=True)
- path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
- def copy(source, name):
- target = PACK / name
- target.parent.mkdir(parents=True, exist_ok=True)
- target.write_bytes(source.read_bytes())
- def capture(name):
- try:
- with urlopen(Request('https://kangshifu.fmode.cn/' + name, headers={'Cache-Control': 'no-cache'}), timeout=35) as response:
- raw = response.read(3000001)
- kind = response.headers.get('Content-Type', '')
- assert len(raw) < 3000001
- exists = name == 'index.html' or ('application/json' in kind if name.endswith('.json') else 'javascript' in kind)
- if not exists:
- assert b'<html' in raw[:500].lower() or b'<!doctype' in raw[:500].lower()
- if exists:
- target = WORK / 'baseline' / name
- target.parent.mkdir(parents=True, exist_ok=True)
- target.write_bytes(raw)
- return dict(path=name, exists=exists, status=response.status, contentType=kind,
- sha256=sha(raw) if exists else None, normalizedSha256=sha(normal(raw)) if exists else None)
- except HTTPError as error:
- assert error.code == 404, (name, error.code)
- return dict(path=name, exists=False, status=404, sha256=None, normalizedSha256=None)
- def build():
- assert not PACK.exists(), 'NEW_RELEASE_REQUIRED'
- acceptance = json.loads((ROOT / 'docs/development-board-20261108/browser-acceptance.json').read_bytes())
- assert acceptance['ok'] and len(acceptance['layouts']) == 88
- required, queue = set(), [name for name in FILES if name.endswith('.js')]
- while queue:
- name = queue.pop()
- if name in required:
- continue
- required.add(name)
- for rel in re.findall(r'(?:from\s*|import\s*)[\'\"]([.][^\'\"]+)[\'\"]', (ROOT / name).read_text(encoding='utf-8')):
- queue.append((ROOT / name).parent.joinpath(rel).resolve().relative_to(ROOT).as_posix())
- required -= set(FILES)
- required.add('src/app.config.js')
- with ThreadPoolExecutor(max_workers=8) as pool:
- observed = list(pool.map(capture, sorted(set(FILES) | required | {'index.html'})))
- by_name = {r['path']: r for r in observed}
- assert by_name['index.html']['sha256'] == HOME, 'HOMEPAGE_CHANGED'
- for name in required:
- assert by_name[name]['exists'], 'MISSING_DEPENDENCY:' + name
- assert by_name[name]['normalizedSha256'] == sha(normal((ROOT / name).read_bytes())), 'UNRELEASED_DEPENDENCY:' + name
- entries, patch = [], []
- for name in sorted(FILES):
- old = by_name[name]
- if name not in ['src/core/industry-navigation.js', 'src/pages/view-industry.js']:
- assert not old['exists'], 'NEW_FILE_ALREADY_PRESENT:' + name
- else:
- assert old['exists'], 'EXISTING_FILE_MISSING:' + name
- raw = normal((ROOT / name).read_bytes())
- target = PACK / 'payload' / name
- target.parent.mkdir(parents=True, exist_ok=True)
- target.write_bytes(raw)
- entries.append(dict(path=name, sha256=sha(raw), allowMissing=not old['exists'],
- priorNormalizedSha256=[old['normalizedSha256']] if old['exists'] else [], observedProductionSha256=old['sha256']))
- before = normal((WORK / 'baseline' / name).read_bytes()).decode() if old['exists'] else ''
- if name.endswith('.js'):
- patch.extend(difflib.unified_diff(before.splitlines(True), raw.decode().splitlines(True), fromfile='public/' + name, tofile='r15/' + name))
- publisher = (ROOT / 'artifacts/ksf-board-mobile-filters-20261108-r14/package/scripts/publish-industry-ui.mjs').read_text(encoding='utf-8')
- line = next(line for line in publisher.splitlines() if line.startswith('export const FILES='))
- publisher = publisher.replace(line, 'export const FILES=Object.freeze(' + json.dumps(sorted(FILES)) + ');')
- publisher = publisher.replace('ksf-board-mobile-filters-20261108-r14', RELEASE)
- (PACK / 'scripts').mkdir(parents=True, exist_ok=True)
- (PACK / 'scripts/publish-industry-ui.mjs').write_text(publisher, encoding='utf-8')
- for name in ['rollback-ui.mjs', 'check-site-modes.mjs']:
- script = (ROOT / 'scripts/release-templates' / name).read_text(encoding='utf-8').replace('ksf-board-mobile-filters-20261108-r14', RELEASE)
- (PACK / 'scripts' / name).write_text(script, encoding='utf-8')
- copy(ROOT / 'scripts/release-templates/verify-development-public.mjs', 'scripts/verify-public.mjs')
- copy(ROOT / 'docs/development-board-20261108/上线执行单.md', '上线执行单.md')
- for name in ['browser-acceptance.json', 'data-publication.json', 'directions-1440.png', 'directions-390.png',
- 'categories-1440.png', 'materials-1440.png', 'standards-1440.png']:
- copy(ROOT / 'docs/development-board-20261108' / name, 'evidence/' + name)
- for name in ['development-references.test.js']:
- text = (ROOT / 'test' / name).read_text(encoding='utf-8').replace('../src/', '../payload/src/').replace('../data/', '../payload/data/')
- (PACK / 'test').mkdir(exist_ok=True)
- (PACK / 'test' / name).write_text(text, encoding='utf-8')
- # Two service imports required by native tests; test-support never enters the web root.
- for name in ['src/utils/router.js']:
- copy(ROOT / name, 'payload/' + name) if name in FILES else copy(ROOT / name, 'test-support/' + name)
- text = (PACK / 'test/development-references.test.js').read_text(encoding='utf-8')
- text = text.replace("import {readDevelopmentReferences} from '../payload/src/services/development-service.js';", '')
- # Native suite can load the actual service through a complete isolated module tree.
- for name in ['src/core/development-references.js', 'src/services/development-service.js']:
- copy(ROOT / name, 'test-support/' + name)
- text = "import {readDevelopmentReferences} from '../test-support/src/services/development-service.js';\n" + text
- (PACK / 'test/development-references.test.js').write_text(text, encoding='utf-8')
- save('package.json', {'type':'module', 'private':True})
- save('deploy/industry-ui-files.json',dict(version='ksf.industry-ui.publish.v1',release=RELEASE,homepageSha256=HOME,files=entries))
- save('evidence/production-baseline.json',dict(observedAt=datetime.now(timezone.utc).isoformat(),files=observed))
- (PACK / 'evidence/ui.patch').write_text(''.join(patch), encoding='utf-8')
- save('RELEASE.json',dict(release=RELEASE,staticFiles=sorted(FILES),expectedChanges=6,expectedNewFiles=4,
- backendFiles=[],databaseIncluded=False,credentialsIncluded=False,serviceRestartRequired=False,
- snapshotPublication=True,productionDeployed=False,paidCalls=0))
- print(json.dumps(dict(ok=True,package=str(PACK),staticFiles=len(FILES),dependencies=len(required),sealed=False)))
- def seal():
- check = json.loads((PACK / 'evidence/package-acceptance.json').read_bytes())
- assert check['ok']
- for name, digest in check['testedFilesSha256'].items():
- assert sha((PACK / name).read_bytes()) == digest, 'TESTED_FILE_CHANGED:' + name
- assert sorted(p.relative_to(PACK / 'payload').as_posix() for p in (PACK / 'payload').rglob('*') if p.is_file()) == sorted(FILES)
- assert not list(PACK.rglob('*.env')) and not list(PACK.rglob('*.sqlite'))
- paths = sorted(p for p in PACK.rglob('*') if p.is_file() and p.name != 'FILES.sha256')
- (PACK / 'FILES.sha256').write_text(''.join(sha(p.read_bytes()) + ' ' + p.relative_to(PACK).as_posix() + '\n' for p in paths), encoding='utf-8')
- bundle_path = OUT / (RELEASE + '.zip')
- with zipfile.ZipFile(bundle_path, 'x', zipfile.ZIP_DEFLATED) as z:
- for p in sorted(PACK.rglob('*')):
- if p.is_file():
- z.write(p,p.relative_to(PACK).as_posix())
- with zipfile.ZipFile(bundle_path) as z:
- assert z.testzip() is None
- for line in z.read('FILES.sha256').decode().splitlines():
- digest,name=line.split(' ',1)
- assert sha(z.read(name))==digest
- (OUT / 'delivery.json').write_text(json.dumps(dict(ok=True,archive=str(bundle_path),bytes=bundle_path.stat().st_size,
- sha256=sha(bundle_path.read_bytes()),productionDeployed=False,paidCalls=0),indent=2)+'\n',encoding='utf-8')
- print((OUT / 'delivery.json').read_text(encoding='utf-8'))
- if __name__ == '__main__':
- import sys
- (seal if '--seal' in sys.argv else build)()
|