diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ac818bd6..0239e711 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -26,6 +26,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + # issue #513: 镜像内 VERSION / pyproject / _FALLBACK_VERSION 从 + # frontend/package.json 派生, 防止发版只改 package.json 时版本漂移。 + - name: 同步版本号 (从 frontend/package.json 派生) + run: python3 scripts/sync_versions.py + - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0abe5a33..634c011f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,6 +98,11 @@ jobs: - name: 检出代码 uses: actions/checkout@v4 + # issue #513: 构建产物内的 VERSION / pyproject / _FALLBACK_VERSION 从 + # frontend/package.json 派生, 防止发版只改 package.json 时版本漂移。 + - name: 同步版本号 (从 frontend/package.json 派生) + run: python3 scripts/sync_versions.py + - name: 安装 pnpm uses: pnpm/action-setup@v4 with: diff --git a/Dockerfile b/Dockerfile index d7974896..45ed89a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -93,6 +93,9 @@ RUN if [ "$USE_CN_MIRROR" = "1" ]; then \ # 三个关键路径, 确保 static / tiers / data 都指向容器内正确位置。 COPY backend/app ./app COPY tiers.yaml /app/tiers.yaml +# 版本号运行时兜底 (issue #513): 非冻结路径 _load_version() 读 +# /app/frontend/package.json, 缺文件会静默回退 _FALLBACK_VERSION 显示旧版本。 +COPY frontend/package.json /app/frontend/package.json ENV STATIC_DIR=/app/static \ TIERS_YAML=/app/tiers.yaml \ DATA_DIR=/app/data \ diff --git a/scripts/sync_versions.py b/scripts/sync_versions.py new file mode 100644 index 00000000..c46d2eb0 --- /dev/null +++ b/scripts/sync_versions.py @@ -0,0 +1,80 @@ +"""版本号统一派生 — 从 frontend/package.json 同步三处伴生版本标记。 + +版本号唯一权威 = frontend/package.json (issue #513: 发版只改它时, VERSION / +backend/pyproject.toml / _FALLBACK_VERSION 三处伴生标记会漂移; Docker 部署 +下运行时读不到 frontend/package.json, 实际生效的是 _FALLBACK_VERSION, 于是 +界面/日志/version API 显示旧版本)。 + +用法: + python3 scripts/sync_versions.py # 幂等写回三处 (无变化则零写入) + python3 scripts/sync_versions.py --check # 只校验不一致并退出码 1 (CI 可选) + +release.yml / docker.yml 在检出后调用本脚本, 保证构建产物内版本一致; +本地改完 package.json 后也可手动跑一次。 +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PKG = ROOT / "frontend" / "package.json" +VERSION_FILE = ROOT / "VERSION" +PYPROJECT = ROOT / "backend" / "pyproject.toml" +APP_INIT = ROOT / "backend" / "app" / "__init__.py" + + +def pkg_version() -> str: + return str(json.loads(PKG.read_text(encoding="utf-8"))["version"]).strip() + + +def _replace_once(text: str, pattern: str, repl: str, path: Path) -> str: + new, n = re.subn(pattern, repl, text, count=1, flags=re.MULTILINE) + if n != 1: + raise SystemExit(f"{path}: 未找到版本标记 (pattern={pattern!r})") + return new + + +def sync(ver: str, *, check: bool = False) -> list[str]: + """幂等写回三处伴生标记, 返回发生变化的文件 (相对仓库根)。""" + # (路径, 匹配单行的正则, 写回的整行) + targets = [ + (VERSION_FILE, r"^[^\n]*\n?", f"v{ver}\n"), + (PYPROJECT, r'^version = "[^"]*"\n', f'version = "{ver}"\n'), + (APP_INIT, r'^_FALLBACK_VERSION = "[^"]*"\n', f'_FALLBACK_VERSION = "{ver}"\n'), + ] + changed: list[str] = [] + for path, pattern, want_line in targets: + old = path.read_text(encoding="utf-8") + new = _replace_once(old, pattern, want_line, path) + if new != old: + changed.append(str(path.relative_to(ROOT))) + if not check: + path.write_text(new, encoding="utf-8") + return changed + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="只报告不一致, 不写文件") + args = parser.parse_args() + + ver = pkg_version() + changed = sync(ver, check=args.check) + if changed: + action = "需要同步" if args.check else "已同步" + print(f"{action} → v{ver}:") + for rel in changed: + print(f" - {rel}") + if args.check: + return 1 + else: + print(f"版本一致: v{ver} (VERSION / pyproject / _FALLBACK_VERSION)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())