mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-31 11:05:36 +00:00
81 lines
4.2 KiB
Python
81 lines
4.2 KiB
Python
"""Check only local build output and its provenance in the GitHub snapshot."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from urllib.parse import unquote, urlsplit
|
|
|
|
BASE = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def check(source, output):
|
|
manifest = json.loads((output / 'asset-manifest.json').read_text())
|
|
info = json.loads((output / 'build-info.json').read_text())
|
|
files = {p.relative_to(output).as_posix(): p for p in output.rglob('*') if p.is_file()}
|
|
errors = []
|
|
expected = {item['path']: item for item in manifest['files']}
|
|
if set(files) != set(expected) | {'asset-manifest.json'}:
|
|
errors.append('发布文件与清单不一致')
|
|
for name, record in expected.items():
|
|
path = output / name
|
|
if not path.is_file() or path.stat().st_size != record['bytes'] or hashlib.sha256(path.read_bytes()).hexdigest() != record['sha256']:
|
|
errors.append('文件清单校验失败:' + name)
|
|
references = 0
|
|
for document in output.rglob('*.md'):
|
|
content = re.sub(r'```[^\n]*\n[\s\S]*?```|<!--[\s\S]*?-->', '', document.read_text())
|
|
for match in re.finditer(r'''\]\((/[^\s)]+)\)|(?:src|href)=["'](/[^"']+)''', content):
|
|
url = match[1] or match[2]
|
|
if url.startswith(('/#', '//')):
|
|
continue
|
|
name = unquote(urlsplit(url).path).lstrip('/')
|
|
if not name:
|
|
continue
|
|
references += 1
|
|
if name not in files and name + '.md' not in files:
|
|
errors.append('缺失引用:' + name)
|
|
for name in re.findall(r'(?:src|href)="(/[^"#]+)"', (output / 'index.html').read_text()):
|
|
if name.split('?')[0].lstrip('/') not in files:
|
|
errors.append('缺失页面资源:' + name)
|
|
forbidden = [name for name in files if set(Path(name).parts) & {
|
|
'.git', '.cache', 'node_modules', 'builder', 'templates', 'upstream', 'remote',
|
|
'tools', 'backups', '__pycache__', 'CLAUDE.md', 'AGENTS.md', '.env',
|
|
}]
|
|
if forbidden:
|
|
errors.append('混入维护文件:' + ', '.join(forbidden))
|
|
images = 0
|
|
for name, path in files.items():
|
|
if name.startswith(('notes/pics/', 'assets/', 'docs/_media/')) and path.suffix.lower() in {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico'}:
|
|
images += 1
|
|
original = source / name
|
|
if not original.is_file() or original.read_bytes() != path.read_bytes():
|
|
errors.append('图片与当前源码不一致:' + name)
|
|
index = json.loads((output / info['searchIndex']).read_text())
|
|
note_names = {p.stem for p in (source / 'notes').glob('*.md')}
|
|
if {Path(doc['path']).name for doc in index['documents']} != note_names:
|
|
errors.append('搜索索引未覆盖当前全部文章')
|
|
legacy = json.loads((BASE / 'templates/legacy-routes.json').read_text())
|
|
for target in legacy.values():
|
|
if not (output / (target.lstrip('/') + '.md')).is_file():
|
|
errors.append('旧入口目标缺失:' + target)
|
|
variants = json.loads((output / 'image-variants.json').read_text()) if (output / 'image-variants.json').exists() else {}
|
|
for name, variant in variants.items():
|
|
for path, key in [(output / name.lstrip('/'), 'sourceSha256'), (output / variant['path'].lstrip('/'), 'sha256')]:
|
|
if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != variant[key]:
|
|
errors.append('图片优化清单不一致:' + name)
|
|
report = {'sourceCommit': info['sourceCommit'], 'notes': len(note_names),
|
|
'files': len(files), 'localReferences': references, 'repositoryImages': images,
|
|
'imageVariants': len(variants), 'legacyRoutes': len(legacy), 'errors': errors}
|
|
return report
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--source', type=Path, default=BASE / 'upstream')
|
|
parser.add_argument('--output', type=Path, default=BASE / 'dist')
|
|
args = parser.parse_args()
|
|
result = check(args.source.resolve(), args.output.resolve())
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
(BASE / 'validation.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n')
|
|
raise SystemExit(bool(result['errors']))
|