#!/usr/bin/env python3 """发布前一致性检查(v2.5.0)。 检查版本号与错误码表在多个文件间是否漂移——历史上 pyproject.toml 与 ``_config.VERSION`` 曾不同步(v2.4.0 发布时前者停在 2.3.0)。发布前运行 本脚本即可一次性发现: 1. 版本一致性:pyproject.toml <-> _config.py(同一来源),并警告 README.md / SKILL.md 中提到的版本号是否与最新版本一致(文档是 手工维护的,仅警告不阻断) 2. 错误码一致性:common.py 的 ``E_*`` 常量 vs 各脚本实际使用 vs RECOVERY_HINTS 覆盖 vs README/SKILL 错误码表中提及的错误码 用法(项目根目录):: python scripts/release_check.py # 完整检查 python scripts/release_check.py --strict # 文档警告也视为失败 零依赖(stdlib),退出码 0=通过 / 1=有阻断问题。 """ import argparse import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SCRIPTS_DIR = ROOT / "scripts" # 文档中可能提及版本号的锚点模式(供提示) _VERSION_HINT_RE = re.compile(r"\b\d+\.\d+\.\d+\b") # 错误码表(README.md 与 SKILL.md 均有)应包含的 E_* 代码,从 common.py # 动态提取,与源码单一来源保持一致。捕获组只取标识符本身(不含 = ")。 _ERROR_CODE_RE = re.compile(r"^\s*(E_[A-Z_]+)\s*=\s*[\"']", re.MULTILINE) def _read(path: Path) -> str: try: return path.read_text(encoding="utf-8") except OSError as e: print(f" [WARN] 无法读取 {path}: {e}") return "" def check_version(strict: bool) -> list: """检查版本号一致性。返回错误消息列表(空 = 通过)。""" errors = [] # 1. pyproject.toml 的 version 字段 pyproject = ROOT / "pyproject.toml" pp = _read(pyproject) m = re.search(r"^version\s*=\s*[\"']([\d.]+)[\"']", pp, re.MULTILINE) pp_version = m.group(1) if m else None # 2. _config.py 的 VERSION(权威来源) import importlib.util spec = importlib.util.spec_from_file_location("_config", SCRIPTS_DIR / "_config.py") cfg = importlib.util.module_from_spec(spec) spec.loader.exec_module(cfg) cfg_version = cfg.VERSION if pp_version and pp_version != cfg_version: errors.append( f"版本漂移: pyproject.toml={pp_version} 但 _config.VERSION={cfg_version}。" f"请同步 pyproject.toml 或改为从 _config 动态读取。" ) elif not pp_version: errors.append("pyproject.toml 缺少 version 字段。") # 3. 文档版本提示(仅警告) if cfg_version: for doc_name in ("README.md", "SKILL.md"): doc = _read(ROOT / doc_name) if not doc: continue mentioned = set(_VERSION_HINT_RE.findall(doc)) # 文档可能同时提到旧版本历史(changelog 里 v2.3.0 等), # 只要"存在"当前版本号即视为已更新 if cfg_version not in mentioned: msg = (f"文档 {doc_name} 未提及当前版本 {cfg_version} " f"(提到: {sorted(mentioned)[:6]}...)。请更新文档。") if strict: errors.append(msg) else: print(f" [WARN] {msg}") return errors def check_error_codes(strict: bool) -> list: """检查错误码表一致性。返回错误消息列表(空 = 通过)。""" errors = [] common_src = _read(SCRIPTS_DIR / "common.py") codes = _ERROR_CODE_RE.findall(common_src) if not codes: errors.append("common.py 中未找到 E_* 常量定义。") return errors codes = sorted(codes) # 1. 每个 E_* 是否有 recovery_hint hint_keys = set(re.findall(r'^\s*"?(E_[A-Z_]+)"?\s*:\s*[\(\["\']', common_src, re.MULTILINE)) missing_hints = [c for c in codes if c not in hint_keys] if missing_hints: errors.append(f"缺少 RECOVERY_HINTS 的错误码: {missing_hints}") # 2. README/SKILL 错误码表是否覆盖所有 E_*(文档漂移提示) for doc_name in ("README.md", "SKILL.md"): doc = _read(ROOT / doc_name) if not doc: continue missing_doc = [c for c in codes if c not in doc] if missing_doc: msg = (f"文档 {doc_name} 错误码表缺少: {missing_doc}。" f"源码已新增这些错误码,请更新文档表格。") if strict: errors.append(msg) else: print(f" [WARN] {msg}") return errors def main() -> int: ap = argparse.ArgumentParser(description="searxng-cli 发布前一致性检查") ap.add_argument("--strict", action="store_true", help="文档警告也视为失败(默认仅提示)") args = ap.parse_args() errors = [] errors += check_version(args.strict) errors += check_error_codes(args.strict) if errors: print("✗ 检查未通过:") for e in errors: print(f" - {e}") return 1 print("✓ 版本号与错误码一致性检查全部通过") return 0 if __name__ == "__main__": sys.exit(main())