diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 00000000..a0aaf901 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,6 @@ +""" +xiaomai-bot核心模块 +""" + +# 版本号(由bump-my-version自动管理) +__version__ = "3.0.0" diff --git "a/docs/\345\217\221\345\270\203 checklist.md" "b/docs/\345\217\221\345\270\203 checklist.md" new file mode 100644 index 00000000..5b2575af --- /dev/null +++ "b/docs/\345\217\221\345\270\203 checklist.md" @@ -0,0 +1,230 @@ +# ✅ 版本发布流程 Checklist + +> 本文档用于规范化版本发布流程,确保每次发布都**一致、可审计、可追溯** +> 当前项目版本管理方案基于: +> +> - [`bump-my-version`](https://github.com/callowayproject/bump-my-version):版本号控制 +> - 自定义脚本 `scripts/bump.py`:自动封装版本更新、changelog 生成、tag 创建、git 提交 +> - [`git-cliff`](https://github.com/orhun/git-cliff):自动生成 changelog +> - 版本号遵循 [Semantic Versioning 2.0.0](https://semver.org/lang/zh-CN/) + +--- + +## 📦 准备发布 + +### ✅ 1. 确保主分支是干净的 + +- 分支切换至主分支(如 `main` 或 `master`): + + ```bash + git checkout main + git pull origin main + ``` + +- 所有待发布的功能已合并进主分支 +- 本地无未提交更改(`git status` 应为空) + +--- + +## 🧰 2. 运行发布脚本 scripts/bump.py + +使用封装好的脚本自动完成以下内容: + +- 更新版本号(修改 `pyproject.toml`、`core/__init__.py` 等) +- 同步 `uv.lock` 中的版本元信息 +- 自动生成 `CHANGELOG.md`(调用 `git-cliff`) +- 使用规范化格式提交变更 +- 自动打 Git tag(如 `v1.2.4`) + +### 🎯 命令示例 + +```bash +# 增加补丁版本号(默认添加预发布标签) +python -m scripts.bump patch --commit --tag --changelog + +# 增加补丁版本号(不添加预发布标签) +python -m scripts.bump patch --no-pre --commit --tag --changelog + +# 使用其他版本级别 +python -m scripts.bump minor --no-pre --commit --tag --changelog +python -m scripts.bump major --no-pre --commit --tag --changelog + +# 递增预发布标签(如 dev → alpha → beta → rc) +python -m scripts.bump pre_l --commit --tag + +# 递增预发布版本号(如 alpha1 → alpha2) +python -m scripts.bump pre_n --commit --tag + +# 发布正式版(移除预发布标签) +python -m scripts.bump release --commit --tag --changelog + +# 直接指定目标版本号(跨版本升级,如从预发布版本直接升级到正式版) +python -m scripts.bump patch --new-version 0.2.0 --commit --tag --changelog + +# 强制更新版本号(当 bump-my-version 自动更新失败时) +python -m scripts.bump patch --new-version 0.2.0 --force --commit --tag +``` + +--- + +## 📜 3. 自动生成的内容 + +### ✅ 提交信息格式 + +```text +chore(release): 版本更新 v1.2.3 → v1.2.4 +``` + +### ✅ 更新的文件包括 + +- `pyproject.toml`:版本号 +- `core/__init__.py`:`__version__` +- `uv.lock`:元信息 version +- `CHANGELOG.md`:根据 git 提交历史生成(分组) + +--- + +## 🏷️ 4. Git tag 自动生成 + +- 生成格式为 `v1.2.4` +- tag 创建在主分支最新提交上 +- 可用于 CI/CD 构建与发布流程触发器 + +--- + +## ☁️ 5. 推送提交与 tag + +完成变更后,统一推送代码和 tag: + +```bash +git push origin main --tags +``` + +--- + +## 🧪 6. 后续验证 + +- CI/CD 会监听 tag 推送并触发构建 +- 可在 Git 平台(如 Gitea、GitHub)中验证: + - tag 是否存在 + - changelog 是否正确 + - changelog 段落是否匹配该版本 + +--- + +## 🔒 7. 注意事项与最佳实践 + +| 项目 | 说明 | +|---------------------|--------------------------------| +| tag 必须在主分支上创建 | 避免 tag 指向临时或未发布的提交 | +| 版本号应与 changelog 对应 | 生成日志前确保 `git log` 包含完整提交 | +| commit message 遵循规范 | `chore(release): 版本更新 vX → vY` | +| 每次发布都 bump | 避免重复使用旧版本号 | +| 将 `uv.lock` 纳入 Git | 保证依赖版本一致,避免构建漂移 | + +--- + +## 🔄 8. 版本号管理策略 + +### 版本号格式 + +我们的版本号遵循如下格式: + +- 标准版本:`X.Y.Z`(例如 `3.0.0`) +- 预发布版本:`X.Y.Z-labelN`(例如 `3.0.1-dev1`、`3.1.0-rc2`) + +### 预发布标签顺序 + +预发布标签遵循如下顺序(由 `pre_l` 命令递增): + +1. `dev`:开发版本,内部开发使用 +2. `alpha`:内部测试版本 +3. `beta`:外部测试版本 +4. `rc`:发布候选版本 + +### 预发布版本流程 + +典型的版本发布流程为: + +1. 开发阶段:`3.0.0` → `3.0.1-dev1` → `3.0.1-dev2`... +2. 内部测试:`3.0.1-alpha1` → `3.0.1-alpha2`... +3. 外部测试:`3.0.1-beta1` → `3.0.1-beta2`... +4. 发布候选:`3.0.1-rc1` → `3.0.1-rc2`... +5. 正式发布:`3.0.1` + +### 何时使用 `--no-pre` 选项 + +使用 `--no-pre` 选项在以下情况下特别有用: + +- 发布紧急修复(hotfix)直接发布正式版本 +- 跳过预发布流程直接发布小功能更新 +- 直接从一个正式版升级到另一个正式版 + +```bash +# 直接升级到下一个补丁版本而不添加预发布标签 +python -m scripts.bump patch --no-pre --commit --tag --changelog +``` + +### 何时使用 `pre_l` 和 `pre_n` 命令 + +这两个命令用于管理预发布版本: + +- `pre_l`(pre-label):递增预发布标签,如 dev → alpha → beta → rc +- `pre_n`(pre-number):递增预发布版本号,如 alpha1 → alpha2 + +```bash +# 将版本从 1.0.0-dev1 升级到 1.0.0-alpha1 +python -m scripts.bump pre_l --commit --tag + +# 将版本从 1.0.0-alpha1 升级到 1.0.0-alpha2 +python -m scripts.bump pre_n --commit --tag +``` + +### 何时使用 `--new-version` 和 `--force` 选项 + +使用 `--new-version` 选项在以下情况下特别有用: + +- 从预发布版本直接升级到指定的正式版本(如 `0.1.1-dev1` → `0.2.0`) +- 跳过多个版本进行升级(如 `1.0.0` → `2.0.0`),不遵循常规版本增量 +- 当需要进行版本号规范化调整时 + +```bash +# 直接指定目标版本号 +python -m scripts.bump patch --new-version 0.2.0 --commit --tag +``` + +当 bump-my-version 自动更新版本号失败时(特别是从预发布版本升级时),可以使用 `--force` 选项强制更新: + +```bash +# 强制更新版本号 +python -m scripts.bump patch --new-version 0.2.0 --force --commit --tag +``` + +--- + +## ✅ 参考命令速查表 + +| 操作 | 封装脚本方式 | 直接命令方式 | +|----------------------------------|------------------------------------------------------------|---------------------------------------------------------------------| +| 增加补丁版本(不带预发布标签) | `python -m scripts.bump patch --no-pre` | `bump-my-version bump patch --serialize "{major}.{minor}.{patch}"` | +| 增加补丁版本(添加预发布标签) | `python -m scripts.bump patch` | `bump-my-version bump patch` | +| 增加次版本(不带预发布标签) | `python -m scripts.bump minor --no-pre` | `bump-my-version bump minor --serialize "{major}.{minor}.{patch}"` | +| 增加主版本(不带预发布标签) | `python -m scripts.bump major --no-pre` | `bump-my-version bump major --serialize "{major}.{minor}.{patch}"` | +| 递增预发布标签(dev → alpha → beta → rc) | `python -m scripts.bump pre_l` | `bump-my-version bump pre_l` | +| 递增预发布版本号(alpha1 → alpha2) | `python -m scripts.bump pre_n` | `bump-my-version bump pre_n` | +| 发布正式版(移除预发布标签) | `python -m scripts.bump release` | `bump-my-version bump pre final` | +| 指定具体版本号 | `python -m scripts.bump patch --new-version X.Y.Z` | `bump-my-version bump --new-version X.Y.Z` | +| 强制更新版本号(当自动更新失败时) | `python -m scripts.bump patch --new-version X.Y.Z --force` | 不支持,需使用脚本 | +| 从预发布版本直接升级到正式版本(如 0.2.0) | `python -m scripts.bump patch --new-version 0.2.0` | `bump-my-version bump --new-version 0.2.0 --current-version "当前版本"` | +| 查看当前版本 | `python -m scripts.bump info` | `bump-my-version show current_version` | + +--- + +## ✅ 推荐工具版本依赖 + +| 工具 | 推荐安装方式 | 说明 | +|-------------------|------------------------------------|------------------------------------| +| `bump-my-version` | `uv tool install bump-my-version` | 使用 uv 工具安装,而非作为项目依赖 | +| `git-cliff` | 下载二进制或使用 `cargo install git-cliff` | 用于生成规范化的 changelog | +| `uv` | 官方安装指南 | 用于快速的依赖管理与 lock 文件更新 | +| `tomli` | 项目依赖 | 用于解析 pyproject.toml(Python < 3.11) | diff --git a/modules/required/status/__init__.py b/modules/required/status/__init__.py index 0d9e6d10..6b7ae805 100644 --- a/modules/required/status/__init__.py +++ b/modules/required/status/__init__.py @@ -20,6 +20,7 @@ from core.config import GlobalConfig from core.control import Distribute, FrequencyLimitation, Function, Permission from core.models import response_model, saya_model +from utils.version_info import get_full_version_info config = create(GlobalConfig) core = create(Umaru) @@ -121,6 +122,9 @@ async def message_counter(): ) @dispatch(Twilight([FullMatch("-bot").space(SpacePolicy.PRESERVE)])) async def status(app: Ariadne, src_place: Group | Friend, source: Source): + # 获取版本信息 + version, git_info, b2v_info = get_full_version_info() + # 运行时长 time_start = int(time.mktime(core.launch_time.timetuple())) m, s = divmod(int(time.time()) - time_start, 60) @@ -145,6 +149,24 @@ async def status(app: Ariadne, src_place: Group | Friend, source: Source): ) real_time_received_message_count = message_count.get_receive_count() real_time_sent_message_count = message_count.get_send_count() + + # 版本信息块 + version_info = f"版本信息:v{version}\n" + if git_info["commit_short"] != "未知": + version_info += f"Git分支:{git_info['branch']}\n" + version_info += ( + f"最新提交:{git_info['commit_short']} ({git_info['commit_author']})\n" + ) + version_info += f"提交信息:{git_info['commit_message']}\n" + + # 构建信息块 + build_info = "" + if b2v_info["build_number"] != "开发环境": + build_info = f"构建编号:{b2v_info['build_number']}\n" + if b2v_info["build_date"]: + build_info += f"构建日期:{b2v_info['build_date']}\n" + build_info += f"构建类型:{b2v_info['build_type']}\n" + await app.send_message( src_place, MessageChain( @@ -158,6 +180,8 @@ async def status(app: Ariadne, src_place: Group | Friend, source: Source): f"在线bot数量:{len([app_item for app_item in core.apps if Ariadne.current(app_item.account).connection.status.available])}/" f"{len(core.apps)}\n", f"活动群组数量:{len(account_controller.total_groups.keys())}\n", + version_info, + build_info, "项目地址:https://github.com/g1331/xiaomai-bot", ), quote=source, diff --git a/pyproject.toml b/pyproject.toml index c9e1c889..2c5c5786 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bot-xiaomai-open" -version = "0.3.0" +version = "3.0.0" description = "GNU GENERAL PUBLIC LICENSE" readme = "README.md" requires-python = ">=3.10,<3.13" @@ -64,20 +64,164 @@ dependencies = [ "restrictedpython>=8.0", "scipy>=1.15.1", "pre-commit>=4.1.0", + "tomli>=2.2.1", ] +[tool.uv] +package = false # ⚠️ 禁用“本项目安装”以便只安装第三方依赖 + +# bump-my-version 配置 +[tool.bumpversion] +current_version = "3.0.0" +commit = false +tag = false +parse = """(?x) + (?P0|[1-9]\\d*)\\. + (?P0|[1-9]\\d*)\\. + (?P0|[1-9]\\d*) + (?: + - # dash separator for pre-release section + (?P[a-zA-Z-]+) # pre-release label + (?P0|[1-9]\\d*) # pre-release version number + )? # pre-release section is optional +""" +serialize = [ + "{major}.{minor}.{patch}-{pre_l}{pre_n}", + "{major}.{minor}.{patch}", +] + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = "version = \"{current_version}\"" +replace = "version = \"{new_version}\"" + +[[tool.bumpversion.files]] +filename = "core/__init__.py" +search = "__version__ = \"{current_version}\"" +replace = "__version__ = \"{new_version}\"" + +[tool.bumpversion.parts.pre_l] +optional_value = "final" +first_value = "dev" +values = ["dev", "alpha", "beta", "rc", "final"] + +[tool.bumpversion.parts.pre_n] +first_value = "1" + +# git-cliff 默认配置文件 +# 官方文档:https://git-cliff.org/docs/configuration +# +# 以下配置以表(table)和键(key)的形式组织 +# 所有以 "#" 开头的行为注释行,不会被执行 + +[tool.git-cliff.changelog] +# changelog 文件头部模板 +header = """ +# 更新日志\n +本文件记录了该项目的所有重要变更。\n +""" + +# changelog 主体模板(使用 Tera 模板语法) +# Tera 模板语法文档:https://keats.github.io/tera/docs/ +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [未发布版本] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**破坏性修改**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %}\n +""" + +# changelog 尾部模板 +footer = """ +--- + +### 🔍 相关链接 +- 问题反馈:[Issues](/issues) +- 项目主页:[g1331/xiaomai-bot]() + +> 本文档由 [git-cliff](https://git-cliff.org) 根据 Git 提交记录自动生成 +""" + +# 是否裁剪头尾空白字符 +trim = true + +# 后处理器(例如替换链接) +postprocessors = [ + { pattern = '', replace = "https://github.com/g1331/xiaomai-bot" }, # 替换仓库地址 +] + +# 即使没有版本也强制渲染 changelog 主体 +# render_always = true + +# 输出文件路径(默认写入 stdout) +output = "CHANGELOG.md" + +[tool.git-cliff.git] +# 使用 conventional commits 规范解析提交日志 +conventional_commits = true + +# 只保留符合规范的提交 +filter_unconventional = true + +# 是否将每一行作为独立提交解析 +split_commits = false + +# 提交消息预处理器(支持正则替换) +commit_preprocessors = [ + # 示例:将 issue 编号替换为链接 + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" }, + # 示例:拼写检查并自动修复(需安装 typos 工具) + # { pattern = '.*', replace_command = 'typos --write-changes -' }, +] + +# 提交分类器,根据提交内容划分分组 +commit_parsers = [ + { message = "^feat", group = "🚀 新特性" }, + { message = "^fix", group = "🐛 修复问题" }, + { message = "^doc", group = "📚 文档相关" }, + { message = "^perf", group = "⚡ 性能优化" }, + { message = "^refactor", group = "🚜 代码重构" }, + { message = "^style", group = "🎨 代码格式" }, + { message = "^test", group = "🧪 测试相关" }, + { message = "^chore\\(release\\): prepare for", skip = true }, # 忽略发布准备类提交 + { message = "^chore\\(deps.*\\)", skip = true }, # 忽略依赖类提交 + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|^ci", group = "⚙️ 其他任务" }, + { body = ".*security", group = "🛡️ 安全相关" }, + { message = "^revert", group = "◀️ 回滚提交" }, + { message = ".*", group = "💼 其他修改" }, +] + +# 是否过滤掉未被上述规则匹配的提交(false 表示保留) +filter_commits = false + +# 是否按 Git 拓扑顺序排序标签 +topo_order = false + +# 每个分类组内的提交排序方式(newest/oldest) +sort_commits = "newest" + # 配置 Ruff 的整体行为 [tool.ruff] -line-length = 88 # Black 默认 88,可以改回 79 如果严格遵守 PEP 8 +line-length = 88 # Black 默认 88,可以改回 79 如果严格遵守 PEP 8 # 配置 Ruff 的代码检查功能(Linting) [tool.ruff.lint] # 选择要启用的规则集 select = [ - "E", # PEP 8 代码格式错误(空格、缩进、换行) - "F", # Pyflakes 代码检查(未使用变量、重复导入、变量未定义等) - "W", # 一般代码风格警告(行尾空格、缩进警告) - "UP" # Python 版本升级检查(建议 Python 3 代码优化) + "E", # PEP 8 代码格式错误(空格、缩进、换行) + "F", # Pyflakes 代码检查(未使用变量、重复导入、变量未定义等) + "W", # 一般代码风格警告(行尾空格、缩进警告) + "UP", # Python 版本升级检查(建议 Python 3 代码优化) ] # 忽略特定规则 @@ -89,10 +233,10 @@ ignore = [ # 代码格式化配置(类似于 Black) [tool.ruff.format] -quote-style = "double" # 统一使用双引号 -indent-style = "space" # 统一使用空格缩进,而不是 Tab -docstring-code-format = true # 启用 docstring 代码片段格式化 +quote-style = "double" # 统一使用双引号 +indent-style = "space" # 统一使用空格缩进,而不是 Tab +docstring-code-format = true # 启用 docstring 代码片段格式化 # 配置 Ruff 的 PyUpgrade 功能(Python 版本兼容性优化) [tool.ruff.lint.pyupgrade] -keep-runtime-typing = true # 保留 `from __future__ import annotations`,避免影响运行时类型检查 +keep-runtime-typing = true # 保留 `from __future__ import annotations`,避免影响运行时类型检查 diff --git a/scripts/bump.py b/scripts/bump.py new file mode 100644 index 00000000..fefecccc --- /dev/null +++ b/scripts/bump.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python +""" +bump.py - 项目版本号自动管理脚本 + +支持: +- 使用 bump-my-version 修改版本 +- 自动同步 uv.lock +- 可选生成 changelog(需安装 git-cliff) +- 可选自动 commit 和打 Git tag + +用法示例: + python -m utils.bump patch --commit --tag --changelog # 更新补丁版本并提交、打标签 + python -m utils.bump pre_l # 递增预发布标签(如 dev → alpha → beta → rc) + python -m utils.bump pre_n # 递增预发布版本号(如 alpha1 → alpha2) + python -m utils.bump patch --no-pre # 直接更新补丁版本,不添加预发布标签 + python -m utils.bump patch --new-version 0.2.0 # 直接指定目标版本号 + python -m utils.bump release # 移除预发布标签,发布正式版 +""" + +import argparse +import os +import re +import shutil +import subprocess +import sys + + +# 检查是否安装了 bump-my-version 作为 uv 工具 +def is_bumpmyversion_installed(): + """检查 bump-my-version 是否已作为 uv 工具安装""" + return shutil.which("bump-my-version") is not None + + +def check_bumpmyversion(): + """检查 bump-my-version 是否已安装""" + if not is_bumpmyversion_installed(): + print("错误: 未安装 bump-my-version,请先运行: uv tool install bump-my-version") + sys.exit(1) + + +def get_current_version(): + """获取当前版本号,优先从 pyproject.toml 读取""" + # 从 pyproject.toml 获取 + try: + import tomli + + with open("pyproject.toml", "rb") as f: + data = tomli.load(f) + # 先检查 tool.bumpversion.current_version + version = data.get("tool", {}).get("bumpversion", {}).get("current_version") + if version: + return version.strip("\"'") + # 再检查 project.version + version = data.get("project", {}).get("version") + if version: + return version.strip("\"'") + except Exception: + pass + + return "未知" + + +def get_base_version(version: str) -> str: + """从版本号中提取基础版本(不含预发布标签)""" + match = re.match(r"\d+\.\d+\.\d+", version) + return match.group(0) if match else version + + +def run_bumpmyversion(part, new_version=None, no_pre=False): + """执行 bump-my-version,不自动 commit 或 tag + + 返回: + tuple: (bool, str) 执行成功返回 (True, 新版本号),失败返回 (False, None) + """ + cmd = ["bump-my-version", "bump"] # 添加 'bump' 子命令 + + # 明确指定当前版本,避免从预发布版本升级时的问题 + current_version = get_current_version() + cmd += ["--current-version", current_version] + + # 初始化 result_version + result_version = None + + if new_version: + cmd += ["--new-version", new_version] + # 如果指定了新版本,就不需要指定要更新的部分了 + part = None + result_version = new_version # 如果指定了新版本,直接使用该版本号 + # 如果指定 no_pre=True,先提取基础版本号,然后根据命令增加相应的版本号部分 + elif no_pre: + base_version = get_base_version(current_version) + + # 根据命令构造新版本号 + if part == "major": + parts = base_version.split(".") + new_ver = f"{int(parts[0]) + 1}.0.0" + elif part == "minor": + parts = base_version.split(".") + new_ver = f"{parts[0]}.{int(parts[1]) + 1}.0" + elif part == "patch": + parts = base_version.split(".") + new_ver = f"{parts[0]}.{parts[1]}.{int(parts[2]) + 1}" + else: + new_ver = base_version + + cmd = [ + "bump-my-version", + "bump", + "--current-version", + current_version, + "--new-version", + new_ver, + ] + result_version = new_ver + part = None + + # 仅当未指定新版本时才添加部分参数 + if part: + cmd.append(part) + + try: + print(f"执行命令: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"❌ bump-my-version 执行失败: {result.stderr or result.stdout}") + return False, None + + print(result.stdout or "✅ 版本号已更新") + + # 如果没有指定新版本号,则从文件中读取 + if new_version is None: + result_version = get_current_version() + + return True, result_version + except Exception as e: + print(f"❌ bump-my-version 出错: {e}") + return False, None + + +def update_pyproject_version_directly(new_version): + """直接更新 pyproject.toml 中的版本号(在 bump-my-version 失败时的备用方案)""" + try: + with open("pyproject.toml", encoding="utf-8") as f: + content = f.read() + + # 更新项目版本 + content = re.sub(r'(version\s*=\s*)"[^"]+"', f'\\1"{new_version}"', content) + + # 更新 bumpversion 配置中的当前版本 + content = re.sub( + r'(current_version\s*=\s*)"[^"]+"', f'\\1"{new_version}"', content + ) + + with open("pyproject.toml", "w", encoding="utf-8") as f: + f.write(content) + + # 同时更新 core/__init__.py + if os.path.exists("core/__init__.py"): + with open("core/__init__.py", encoding="utf-8") as f: + content = f.read() + + content = re.sub( + r'(__version__\s*=\s*)"[^"]+"', f'\\1"{new_version}"', content + ) + + with open("core/__init__.py", "w", encoding="utf-8") as f: + f.write(content) + + print(f"✅ 已直接更新版本号至 {new_version}") + return True + except Exception as e: + print(f"❌ 直接更新版本号失败: {e}") + return False + + +def update_uv_lock(): + """更新 uv.lock 文件""" + if not os.path.exists("uv.lock"): + print("⚠️ 未找到 uv.lock,跳过") + return + try: + result = subprocess.run(["uv", "lock"], capture_output=True, text=True) + if result.returncode != 0: + print(f"❌ uv lock 执行失败: {result.stderr}") + sys.exit(1) + print("✅ uv.lock 已更新") + except Exception as e: + print(f"❌ 执行 uv lock 出错: {e}") + sys.exit(1) + + +def generate_changelog(version: str): + """生成 changelog""" + cmd = [ + "git-cliff", + "--tag", + f"v{version}", + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"❌ changelog 生成失败: {result.stderr}") + sys.exit(1) + print("✅ changelog 已更新并追加到 CHANGELOG.md") + except Exception as e: + print(f"❌ 执行 git-cliff 出错: {e}") + sys.exit(1) + + +def git_commit_and_tag(prev_version: str, new_version: str, tag: bool): + """提交更改并创建 Git tag""" + files = ["pyproject.toml", "uv.lock"] + if os.path.exists("core/__init__.py"): + files.append("core/__init__.py") + if os.path.exists("CHANGELOG.md"): + files.append("CHANGELOG.md") + + subprocess.run(["git", "add"] + files, check=True) + message = f"chore(release): bump version v{prev_version} → v{new_version}" + subprocess.run(["git", "commit", "-m", message], check=True) + + if tag: + subprocess.run(["git", "tag", f"v{new_version}"], check=True) + print(f"✅ Git tag v{new_version} 已创建") + + +def main(): + parser = argparse.ArgumentParser( + description="项目版本号自动更新脚本(基于 bump-my-version)", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "command", + choices=[ + "major", + "minor", + "patch", + "pre_l", + "pre_n", + "release", + "info", + ], + help=( + "版本操作命令:\n" + " major 主版本号 +1,如 1.2.3 → 2.0.0\n" + " minor 次版本号 +1,如 1.2.3 → 1.3.0\n" + " patch 补丁号 +1,如 1.2.3 → 1.2.4\n" + " pre_l 递增预发布标签,如 dev → alpha → beta → rc\n" + " pre_n 递增预发布版本号,如 alpha1 → alpha2\n" + " release 移除预发布标签,发布正式版\n" + " info 显示当前版本信息" + ), + ) + parser.add_argument("--tag", action="store_true", help="创建 Git tag(如 v1.2.4)") + parser.add_argument( + "--commit", action="store_true", help="自动提交所有版本变更文件" + ) + parser.add_argument( + "--changelog", action="store_true", help="生成 changelog(依赖 git-cliff)" + ) + parser.add_argument( + "--no-pre", action="store_true", help="直接更新版本号,不添加预发布标签" + ) + parser.add_argument("--new-version", help="直接指定新的版本号(例如:0.2.0)") + parser.add_argument( + "--force", + action="store_true", + help="强制直接修改文件(当 bump-my-version 失败时使用)", + ) + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(0) + + args = parser.parse_args() + + if args.command == "info": + version = get_current_version() + print(f"当前版本号: v{version}") + return + + check_bumpmyversion() + + prev_version = get_current_version() + new_version = None + + # 处理明确指定的新版本号 + if args.new_version: + print(f"⚙️ 准备将版本从 {prev_version} 更新到 {args.new_version}") + success, new_version = run_bumpmyversion(None, new_version=args.new_version) + if not success: + print("❌ 版本更新失败") + if args.force: + print("⚠️ bump-my-version 失败,尝试直接修改文件...") + update_pyproject_version_directly(args.new_version) + new_version = args.new_version + else: + print("❌ 如果你确定要强制更新,请添加 --force 参数") + sys.exit(1) + else: + # 处理预发布版本和标准版本更新 + if args.command == "release": + # 移除预发布标签,发布正式版 + base = get_base_version(prev_version) + success, new_version = run_bumpmyversion(None, new_version=base) + if not success: + print("❌ 移除预发布标签失败") + if args.force: + print("⚠️ bump-my-version 失败,尝试直接修改文件...") + update_pyproject_version_directly(base) + new_version = base + else: + print("❌ 如果你确定要强制更新,请添加 --force 参数") + sys.exit(1) + else: + # 直接使用 bump-my-version 的内置功能处理所有版本更新 + # 包括 major、minor、patch、pre_l、pre_n 等 + success, new_version = run_bumpmyversion(args.command, no_pre=args.no_pre) + if not success: + print("❌ 版本更新失败") + if args.force and args.command in ["major", "minor", "patch"]: + print("⚠️ bump-my-version 失败,尝试直接修改文件...") + # 如果是标准版本更新,可以尝试直接修改文件 + # 计算新版本号 + parts = prev_version.split(".") + if args.command == "major": + new_ver = f"{int(parts[0]) + 1}.0.0" + elif args.command == "minor": + new_ver = f"{parts[0]}.{int(parts[1]) + 1}.0" + elif args.command == "patch": + new_ver = ( + f"{parts[0]}.{parts[1]}.{int(parts[2].split('-')[0]) + 1}" + ) + update_pyproject_version_directly(new_ver) + new_version = new_ver + else: + print("❌ 如果你确定要强制更新,请添加 --force 参数") + sys.exit(1) + + # 如果没有获取到新版本号,则尝试从文件中读取 + if new_version is None: + new_version = get_current_version() + + # 更新 uv.lock + update_uv_lock() + + # 生成 changelog(如指定) + if args.changelog: + generate_changelog(new_version) + + # 提交和打 tag(如指定) + if args.commit: + git_commit_and_tag(prev_version, new_version, tag=args.tag) + + +if __name__ == "__main__": + main() diff --git a/utils/version_info.py b/utils/version_info.py new file mode 100644 index 00000000..6c62a08d --- /dev/null +++ b/utils/version_info.py @@ -0,0 +1,102 @@ +import os +import sys +from pathlib import Path + +from loguru import logger + +# 项目根目录 +ROOT_DIR = Path(__file__).parent.parent + +# 从core.__init__中获取版本信息 +try: + sys.path.insert(0, str(ROOT_DIR)) + from core import __version__ +except ImportError as e: + logger.error(f"从core模块导入版本信息失败: {e}") + # 回退方案:尝试从pyproject.toml读取 + try: + import tomli + + with open(ROOT_DIR / "pyproject.toml", "rb") as f: + data = tomli.load(f) + __version__ = data.get("project", {}).get("version", "0.0.0") + except Exception as e: + logger.error(f"读取版本信息失败: {e}") + __version__ = "0.0.0" +finally: + # 确保恢复sys.path + if str(ROOT_DIR) in sys.path: + sys.path.remove(str(ROOT_DIR)) + +try: + from git import Repo + + has_git = True +except ImportError: + logger.warning("未检测到GitPython库,无法获取git信息") + Repo = None + has_git = False + + +def get_version() -> str: + """获取项目版本号""" + return __version__ + + +def get_git_info() -> dict[str, str]: + """获取git仓库信息""" + result = { + "branch": "未知分支", + "commit": "未知提交", + "commit_short": "未知", + "commit_time": "未知时间", + "commit_author": "未知作者", + "commit_message": "未知消息", + } + + if not has_git: + return result + + try: + repo = Repo(ROOT_DIR) + if repo.bare: + return result + + commit = repo.head.commit + result.update( + { + "branch": repo.active_branch.name, + "commit": commit.hexsha, + "commit_short": commit.hexsha[:7], + "commit_time": str(commit.authored_datetime), + "commit_author": commit.author.name, + "commit_message": commit.message.strip(), + } + ) + except Exception as e: + logger.error(f"获取git信息失败: {e}") + + return result + + +def get_b2v() -> dict[str, str]: + """获取b2v的配置(build to version) + + 用于显示构建信息,如果项目使用CI/CD部署,可以通过环境变量设置 + """ + return { + "build_number": os.environ.get("B2V_BUILD_NUMBER", "开发环境"), + "build_date": os.environ.get("B2V_BUILD_DATE", ""), + "build_type": os.environ.get("B2V_BUILD_TYPE", "dev"), + } + + +def get_full_version_info() -> tuple[str, dict[str, str], dict[str, str]]: + """获取完整的版本信息 + + 返回: + version: 版本号 + git_info: git仓库信息 + b2v_info: 构建信息 + """ + return get_version(), get_git_info(), get_b2v() diff --git a/uv.lock b/uv.lock index d2dbf7d1..5486901d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10, <3.13" resolution-markers = [ "python_full_version < '3.11'", @@ -247,7 +248,7 @@ wheels = [ [[package]] name = "bot-xiaomai-open" -version = "0.3.0" +version = "3.0.0" source = { virtual = "." } dependencies = [ { name = "aiofiles" }, @@ -304,6 +305,7 @@ dependencies = [ { name = "selenium" }, { name = "sqlalchemy" }, { name = "starlette" }, + { name = "tomli" }, { name = "unwind" }, { name = "uvicorn" }, { name = "webdriver-manager" }, @@ -366,6 +368,7 @@ requires-dist = [ { name = "selenium", specifier = ">=4.28.1" }, { name = "sqlalchemy", specifier = ">=2.0.30" }, { name = "starlette", specifier = ">=0.37.2" }, + { name = "tomli", specifier = ">=2.2.1" }, { name = "unwind", specifier = "==0.4.0" }, { name = "uvicorn", specifier = ">=0.29.0" }, { name = "webdriver-manager", specifier = ">=4.0.2" }, @@ -501,7 +504,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -2029,8 +2032,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, - { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 }, - { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 }, { url = "https://files.pythonhosted.org/packages/08/16/ae464d4ac338c1dd41f89c41f9488e54f7d2a3acf93bb920bb193b99f8e3/pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8", size = 1615855 }, { url = "https://files.pythonhosted.org/packages/1e/8c/b0cee957eee1950ce7655006b26a8894cee1dc4b8747ae913684352786eb/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6", size = 1650018 }, { url = "https://files.pythonhosted.org/packages/93/4d/d7138068089b99f6b0368622e60f97a577c936d75f533552a82613060c58/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0", size = 1687977 }, @@ -2703,12 +2704,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/cd/5c735818692927e07980357445569adb6ee204c3332d19c516bae01c6cfa/tls_client-1.0.1-py3-none-any.whl", hash = "sha256:2f8915c0642c2226c9e33120072a2af082812f6310d32f4ea4da322db7d3bb1c", size = 41287556 }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + [[package]] name = "tqdm" version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [