| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- """Freeze the committed product overview patch, without private databases or credentials."""
- 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 = 'f4bdaa70a1f9ceac7c44f79a32d7454143875fd5'
- NAME = 'ksf-product-overview-20261106-r1'
- FRONT = ['src/core/library.js', 'src/pages/library-product-detail.js']
- BACK = ['server/voc-library/' + n for n in ('board_library.py', 'product_insights.py', 'update_library.py', 'agent_batch.py')]
- DEPS = ['server/voc-library/' + n for n in ('voc_db.py', 'retrieval.py', 'schema.sql', 'taxonomy.json')]
- 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'); p.add_argument('--homepage-file', required=True)
- args = p.parse_args()
- commit = subprocess.check_output(['git', 'rev-parse', args.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(name, body):
- dest = package / name; dest.parent.mkdir(parents=True, exist_ok=True); dest.write_bytes(body)
- def js(name, value): write(name, (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode())
- for name in FRONT + BACK: write('payload/' + name, frozen.read(name))
- for name in frozen.namelist():
- if name.startswith('skills/') and not name.endswith('/'): write(name, frozen.read(name))
- write('install-query-skills.py', frozen.read('scripts/install-query-skills.py'))
- baselines = {name: [{p.split('/', 2)[2]: sha(old.read(p)) for p in old.namelist() if p.startswith('skills/' + name + '/') and not p.endswith('/')}]
- for name in ('ksf-workshop-runtime', 'ksf-voc-library')}
- js('baselines.json', baselines)
- def entry(name):
- prior = old.read(name) if name in old.namelist() else None
- return dict(path=name, sha256=sha(frozen.read(name)), priorNormalizedSha256=[norm(prior)] if prior else [], allowMissing=prior is None)
- manifest = dict(version='ksf.product-overview.publish.v1', release=NAME,
- homepageSha256=json.loads(old.read('deploy/board-files-20261106.json'))['homepageSha256'], files=[entry(n) for n in FRONT])
- js('deploy/board-files.json', manifest); js('deploy/backend-files.json', dict(files=[entry(n) for n in BACK]))
- js('deploy/dependencies.json', dict(files=[dict(path=n, normalizedSha256=norm(frozen.read(n))) for n in DEPS]))
- publisher = frozen.read('scripts/publish-opportunity-ui.mjs').decode()
- start = publisher.index('export const FILES=Object.freeze(['); end = publisher.index(']);', start) + 3
- publisher = publisher[:start] + 'export const FILES=Object.freeze(' + json.dumps(FRONT) + ');' + publisher[end:]
- publisher = publisher.replace('ksf.opportunity-ui.publish.v1', manifest['version']).replace('ksf.opportunity-ui.20261103.v1', NAME)
- publisher = publisher.replace('filesChecked:6', 'filesChecked:FILES.length').replace('ksf-opportunity-ui', 'ksf-product-overview')
- publisher = publisher.replace("sourceDir:root,manifestPath:path.join(root,'deploy/opportunity-ui-files-20261103.json')", "sourceDir:path.join(root,'payload'),manifestPath:path.join(root,'deploy/board-files.json')")
- write('scripts/publish-board.mjs', publisher.encode())
- write('发给运维001.md', frozen.read('docs/product-overview-20261106/发给运维001.md'))
- skills = {}
- for name in baselines:
- folder = package / 'skills' / name
- lines = (folder / 'FILES.sha256').read_text(encoding='utf-8').splitlines()
- for line in lines:
- h, rel = line.split(' ', 1); assert sha((folder / rel).read_bytes()) == h
- skills[name] = dict(entries=len(lines), manifestSha256=sha((folder / 'FILES.sha256').read_bytes()))
- # Exercise the actual delivered installer against both frozen baseline skills.
- spec = importlib.util.spec_from_file_location('overview_skill_installer', package / 'install-query-skills.py')
- installer = importlib.util.module_from_spec(spec); spec.loader.exec_module(installer)
- with tempfile.TemporaryDirectory(prefix='installer-check-', dir=package.parent) as td:
- data = Path(td)
- for name in baselines:
- for rel in baselines[name][0]:
- target = data / 'skills' / name / rel; target.parent.mkdir(parents=True, exist_ok=True)
- target.write_bytes(old.read('skills/' + name + '/' + rel))
- plan = installer.install(package, data)
- assert plan['filesChecked'] == 25 and len(plan['changed']) == 6
- assert all(n.startswith('ksf-voc-library/') for n in plan['changed'])
- assert installer.install(package, data, True)['filesystemVerified']
- assert not installer.install(package, data, True)['applied']
- # Check the two-file publisher plan/apply/no-op against the actual baseline.
- with tempfile.TemporaryDirectory(prefix='static-check-', dir=package.parent) as td:
- target = Path(td)
- for name in ['index.html', *FRONT]:
- dest = target / name; dest.parent.mkdir(parents=True, exist_ok=True)
- dest.write_bytes(old.read(name))
- # The maintained homepage is external to Git's local preview page.
- index = Path(args.homepage_file).read_bytes()
- assert sha(index) == manifest['homepageSha256'], 'HOMEPAGE_MISMATCH'
- (target / 'index.html').write_bytes(index)
- cli = ['node', str(package / 'scripts/publish-board.mjs'), '--target-dir', str(target)]
- result = json.loads(subprocess.check_output(cli, cwd=ROOT))
- assert result['changes'] == FRONT
- assert json.loads(subprocess.check_output([*cli, '--apply'], cwd=ROOT))['filesystemVerified']
- assert not json.loads(subprocess.check_output([*cli, '--apply'], cwd=ROOT))['changes']
- js('RELEASE.json', dict(release=NAME, sourceCommit=commit, baselineCommit=BASE, skills=skills,
- staticFiles=2, backendFiles=4, databaseIncluded=False, databaseMigrationRequired=False,
- credentialsIncluded=False, productionInstalled=False, productionBrowserVerified=False,
- installerCheck=True, publisherCheck=True))
- 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, name = line.split(' ', 1); assert sha(z.read(name)) == 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')
- shutil.copyfile(package / '发给运维001.md', out / '发给运维001.md')
- print(json.dumps(receipt, ensure_ascii=False, indent=2))
- if __name__ == '__main__': main()
|