Skip to content

Commit eb1097c

Browse files
committed
chore(release): 版本升到 0.1.1 + 版本号收敛为单一真源
为 v0.1.1 发版做准备。 版本号此前散落在 5 处(pyproject.toml / src/main.py 的 FastAPI(version=) / web/package.json / README 的 docker pull 示例 / publish.yml 的示例文案), 升版必漏——`/openapi.json` 会报出与镜像 tag 不一致的版本,排查问题时误导。 - 新增 src/version.py:pyproject.toml 为唯一真源,运行时读取 (tomllib,相对本文件定位项目根,容器里 /app/pyproject.toml 已 COPY); 读不到/内容损坏/字段缺失一律回落到 FALLBACK_VERSION,绝不抛异常 - src/main.py 的 FastAPI(version=) 改用 app_version() - 升版 0.1.0 → 0.1.1:pyproject.toml、web/package.json、README、 publish.yml 示例、FALLBACK_VERSION 同步 - 新增防漂移测试:断言五处版本一致,且 src/ 下不得再硬编码版本号 验证:965 测试 / 100% 行+分支覆盖;ruff、tsc、前端 104 测试全通过。
1 parent ac0309c commit eb1097c

9 files changed

Lines changed: 153 additions & 6 deletions

File tree

‎.github/workflows/publish.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
workflow_dispatch:
77
inputs:
88
tag:
9-
description: '要打的版本号(如 v0.1.0,留空则跳过)'
9+
description: '要打的版本号(如 v0.1.1,留空则跳过)'
1010
required: false
1111
default: ''
1212

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ docker compose up -d
5555

5656
```bash
5757
docker compose pull
58-
# 或指定版本:docker pull ghcr.io/robbsluo/coding2api:v0.1.0
58+
# 或指定版本:docker pull ghcr.io/robbsluo/coding2api:v0.1.1
5959
```
6060

6161
推送新版本:打 tag `v*` 推到 main 即触发 publish workflow(见 `.github/workflows/publish.yml`),同时打 `<tag>` 和 `:latest` 到 GHCR。也可在 Actions 页面手动触发(填版本号)。

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "coding2api"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "把 CodeBuddy 与 TRAE SOLO 上游通道统一封装为 OpenAI 兼容 API"
55
readme = "README.md"
66
requires-python = ">=3.12"

‎src/main.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from .stats.query import StatsQuery
4343
from .tasks.pacer import Pacer
4444
from .tasks.runner import build_runner
45+
from .version import app_version
4546
from .webapp import limits as _limits
4647
from .webapp import static as _static
4748
from .webapp.handlers import register_exception_handlers
@@ -177,7 +178,7 @@ async def lifespan(app_: FastAPI):
177178
await closer()
178179
db.close()
179180

180-
app = FastAPI(title="Coding2API", version="0.1.0", lifespan=lifespan)
181+
app = FastAPI(title="Coding2API", version=app_version(), lifespan=lifespan)
181182
# BodySizeLimitMiddleware 必须在最外层:FastAPI.add_middleware 会把后加
182183
# 的包在更外层,所以它在最后添加(见 build_app 末尾)。
183184
app.state.settings = config

‎src/version.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""单一版本源:一切对外暴露的版本号都从这里取。
2+
3+
此前版本号散落在 5 处(pyproject.toml / src/main.py 的 FastAPI(version=) /
4+
web/package.json / README 的 docker pull 示例 / publish.yml 的提示文案),
5+
升版时必漏——`/openapi.json` 会报出与镜像 tag 不一致的版本,排查问题时误导。
6+
7+
规则:**pyproject.toml 是唯一真源**,运行时读取;读不到时(精简镜像、测试临时
8+
目录)回落到已发布版本号,绝不抛异常——版本号是展示信息,不该让服务起不来。
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from functools import lru_cache
14+
from pathlib import Path
15+
16+
# 回落值:仅在读不到 pyproject.toml 时使用。升版时与 pyproject 一起改。
17+
FALLBACK_VERSION = "0.1.1"
18+
19+
# 相对本文件定位项目根:src/version.py → 上一级即项目根
20+
_PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml"
21+
22+
23+
@lru_cache(maxsize=1)
24+
def app_version() -> str:
25+
"""读取项目版本号(pyproject.toml 为唯一真源,读不到则回落)。"""
26+
import tomllib
27+
28+
try:
29+
with _PYPROJECT.open("rb") as handle:
30+
data = tomllib.load(handle)
31+
except (OSError, ValueError):
32+
return FALLBACK_VERSION
33+
version = data.get("project", {}).get("version")
34+
return version if isinstance(version, str) and version else FALLBACK_VERSION

‎tests/test_deployment_assets.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,43 @@ def test_webapp_logging_module_does_not_open_files():
273273
assert "RotatingFileHandler" not in src
274274
assert "FileHandler" not in src
275275
assert "StreamHandler" in src
276+
277+
278+
def test_version_is_single_sourced_everywhere():
279+
"""版本号必须处处一致:pyproject(真源)/ 前端 / 回落常量 / README / publish 示例。
280+
281+
此前这些位置各写一遍,升版必漏——`/openapi.json` 会报出与镜像 tag 不同的
282+
版本,排查问题时误导。这条测试就是防止再漂移。
283+
"""
284+
import json
285+
import re
286+
import tomllib
287+
from pathlib import Path
288+
289+
from src.version import FALLBACK_VERSION, app_version
290+
291+
root = Path(__file__).resolve().parent.parent
292+
with (root / "pyproject.toml").open("rb") as handle:
293+
canonical = tomllib.load(handle)["project"]["version"]
294+
assert app_version() == canonical
295+
296+
package_json = json.loads((root / "web" / "package.json").read_text(encoding="utf-8"))
297+
assert package_json["version"] == canonical, "web/package.json 版本未同步"
298+
299+
assert canonical == FALLBACK_VERSION, "src/version.py 回落值未同步"
300+
301+
readme = (root / "README.md").read_text(encoding="utf-8")
302+
pulls = re.findall(r"coding2api:v(\d+\.\d+\.\d+)", readme)
303+
assert pulls, "README 里找不到带版本的 docker pull 示例"
304+
assert set(pulls) == {canonical}, "README 的 docker pull 示例版本未同步"
305+
306+
publish = (root / ".github" / "workflows" / "publish.yml").read_text(encoding="utf-8")
307+
examples = re.findall(r"如 v(\d+\.\d+\.\d+)", publish)
308+
assert set(examples) == {canonical}, "publish.yml 的示例版本未同步"
309+
310+
# 源码里不得再出现硬编码的版本号(除了单一版本源自己的回落常量)
311+
for path in (root / "src").rglob("*.py"):
312+
if path.name == "version.py":
313+
continue
314+
text = path.read_text(encoding="utf-8")
315+
assert f'version="{canonical}"' not in text, f"{path} 硬编码了版本号"

‎tests/test_m0_coverage.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,75 @@ def test_database_skips_mkdir_for_bare_filename(tmp_path, monkeypatch):
209209
apply_schema(database.connect())
210210
assert (tmp_path / "bare.sqlite3").exists()
211211
database.close()
212+
213+
214+
# --------------------------------------------------------- 单一版本源
215+
216+
def test_app_version_reads_pyproject():
217+
"""版本号从 pyproject.toml 读取(唯一真源),与 tag 同源。"""
218+
import tomllib
219+
220+
from src.version import app_version
221+
222+
with open("pyproject.toml", "rb") as handle:
223+
expected = tomllib.load(handle)["project"]["version"]
224+
assert app_version() == expected
225+
# 带 v 前缀的 tag 去掉 v 后应与之一致(发布流程靠这个对齐)
226+
assert expected.count(".") == 2 and not expected.startswith("v")
227+
228+
229+
def test_app_version_falls_back_when_pyproject_missing(monkeypatch, tmp_path):
230+
"""读不到 pyproject 时回落到常量,绝不抛异常(版本号不该让服务起不来)。"""
231+
import src.version as version_module
232+
233+
monkeypatch.setattr(version_module, "_PYPROJECT", tmp_path / "nope.toml")
234+
version_module.app_version.cache_clear()
235+
try:
236+
assert version_module.app_version() == version_module.FALLBACK_VERSION
237+
finally:
238+
version_module.app_version.cache_clear()
239+
240+
241+
def test_app_version_falls_back_on_corrupt_pyproject(monkeypatch, tmp_path):
242+
"""pyproject 内容损坏(非法 TOML)同样回落,不抛异常。"""
243+
import src.version as version_module
244+
245+
broken = tmp_path / "pyproject.toml"
246+
broken.write_text("this is not = = toml", encoding="utf-8")
247+
monkeypatch.setattr(version_module, "_PYPROJECT", broken)
248+
version_module.app_version.cache_clear()
249+
try:
250+
assert version_module.app_version() == version_module.FALLBACK_VERSION
251+
finally:
252+
version_module.app_version.cache_clear()
253+
254+
255+
def test_app_version_falls_back_on_missing_or_blank_field(monkeypatch, tmp_path):
256+
"""缺 [project].version、版本为空串、version 非字符串 → 全部回落。"""
257+
import src.version as version_module
258+
259+
for content in ('[project]\nname = "x"\n',
260+
'[project]\nversion = ""\n',
261+
'[project]\nversion = 123\n'):
262+
broken = tmp_path / "pyproject.toml"
263+
broken.write_text(content, encoding="utf-8")
264+
monkeypatch.setattr(version_module, "_PYPROJECT", broken)
265+
version_module.app_version.cache_clear()
266+
try:
267+
assert version_module.app_version() == version_module.FALLBACK_VERSION
268+
finally:
269+
version_module.app_version.cache_clear()
270+
271+
272+
def test_app_version_is_cached():
273+
"""重复调用只解析一次(lru_cache)。"""
274+
from src.version import app_version
275+
276+
assert app_version() is app_version()
277+
278+
279+
def test_openapi_reports_app_version():
280+
"""FastAPI 的 version 必须来自单一版本源(此前硬编码 0.1.0 会与 tag 漂移)。"""
281+
from src.main import app_version
282+
283+
assert app_version() # 可调用且非空(main 里的 import 绑定没断)

‎uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎web/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "coding2api-web",
33
"private": true,
4-
"version": "0.1.0",
4+
"version": "0.1.1",
55
"packageManager": "pnpm@10.18.0",
66
"type": "module",
77
"scripts": {

0 commit comments

Comments
 (0)