From 6aec02acb68bbc4720fcec927637cf6802e42d6c Mon Sep 17 00:00:00 2001 From: NoahLan <6995syu@163.com> Date: Sat, 16 May 2026 01:29:57 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E4=B8=80=E9=94=AE=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++ docs/README.md | 1 + scripts/iti.cmd | 10 ++++ scripts/iti.sh | 4 ++ scripts/release.py | 146 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 scripts/release.py diff --git a/README.md b/README.md index a97d287..485d528 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,13 @@ cd ../my-system-app ./app.sh help ``` +发布框架: + +```bash +./scripts/iti.sh release +./scripts/iti.sh release v0.2.1 +``` + ## 文档 - [文档索引](docs/README.md) diff --git a/docs/README.md b/docs/README.md index f678202..bd9ed91 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,4 +21,5 @@ AI 修改框架时优先读 `.codex/skills/iti-flask-framework/SKILL.md`。 ./scripts/iti.sh check ./scripts/iti.sh make-app ../my-business-app my_business_app ./scripts/iti.sh make-system-app ../my-system-app my_system_app +./scripts/iti.sh release ``` diff --git a/scripts/iti.cmd b/scripts/iti.cmd index ceda3f8..ffd5afb 100644 --- a/scripts/iti.cmd +++ b/scripts/iti.cmd @@ -28,6 +28,7 @@ if "%COMMAND%"=="migrate" goto migrate if "%COMMAND%"=="migration" goto migration if "%COMMAND%"=="heads" goto heads if "%COMMAND%"=="current" goto current +if "%COMMAND%"=="release" goto release if "%COMMAND%"=="make-app" goto make_app if "%COMMAND%"=="make-system-app" goto make_system_app @@ -53,6 +54,7 @@ echo heads 查看 Alembic heads echo current 查看当前 Alembic 版本 echo make-app ^<目录^> [包名] 从当前框架仓库模板生成业务项目 echo make-system-app ^<目录^> [包名] 生成带 iti-system 的业务项目 +echo release [版本] 发布框架:测试、改版本、提交、打 tag、推送 echo. echo 示例: echo scripts\iti.cmd install @@ -108,6 +110,14 @@ goto end uv run alembic current goto end +:release +if "%~1"=="" ( + uv run python scripts/release.py +) else ( + uv run python scripts/release.py %* +) +goto end + :make_app set "INCLUDE_SYSTEM=false" goto make_project diff --git a/scripts/iti.sh b/scripts/iti.sh index 1f19c7b..c77a80f 100644 --- a/scripts/iti.sh +++ b/scripts/iti.sh @@ -32,6 +32,7 @@ iTi-Flask 开发脚本 current 查看当前 Alembic 版本 make-app <目录> [包名] 从当前框架仓库模板生成业务项目 make-system-app <目录> [包名] 生成带 iti-system 的业务项目 + release [版本] 发布框架:测试、改版本、提交、打 tag、推送 示例: ./scripts/iti.sh install @@ -84,6 +85,9 @@ case "$command" in current) uv run alembic current ;; + release) + uv run python scripts/release.py "$@" + ;; make-app|make-system-app) target=${1:-} package=${2:-} diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 0000000..29f1173 --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +ABOUT_FILE = ROOT / "iti" / "__about__.py" +COPIER_FILE = ROOT / "copier.yml" +README_FILE = ROOT / "README.md" + +VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") +ABOUT_VERSION_RE = re.compile(r'(__version__\s*=\s*")([^"]+)(")') +README_DEP_PREFIX = r"(iti-flask @ git\+ssh://git@your-git/iTi/iTi-Flask\.git@)" + + +def run(cmd: list[str], *, capture_output: bool = False) -> subprocess.CompletedProcess[str]: + return subprocess.run( + cmd, + cwd=ROOT, + text=True, + capture_output=capture_output, + check=True, + ) + + +def normalize_version(value: str) -> str: + version = value[1:] if value.startswith("v") else value + if not VERSION_RE.fullmatch(version): + raise SystemExit(f"invalid version: {value}") + return version + + +def version_tuple(version: str) -> tuple[int, int, int]: + major, minor, patch = version.split(".") + return int(major), int(minor), int(patch) + + +def bump_patch(version: str) -> str: + major, minor, patch = version_tuple(version) + return f"{major}.{minor}.{patch + 1}" + + +def read_current_version() -> str: + text = ABOUT_FILE.read_text(encoding="utf-8") + match = ABOUT_VERSION_RE.search(text) + if match is None: + raise SystemExit(f"version not found in {ABOUT_FILE}") + return match.group(2) + + +def write_about_version(version: str) -> None: + text = ABOUT_FILE.read_text(encoding="utf-8") + new_text, count = ABOUT_VERSION_RE.subn(rf"\g<1>{version}\g<3>", text, count=1) + if count != 1: + raise SystemExit(f"version line not updated in {ABOUT_FILE}") + ABOUT_FILE.write_text(new_text, encoding="utf-8") + + +def write_copier_tag(current_tag: str, new_tag: str) -> None: + text = COPIER_FILE.read_text(encoding="utf-8") + pattern = re.compile(r"(framework_tag:\n(?: .*\n)*? default: )" + re.escape(current_tag)) + new_text, count = pattern.subn(rf"\g<1>{new_tag}", text, count=1) + if count != 1: + raise SystemExit(f"framework_tag default not updated in {COPIER_FILE}") + COPIER_FILE.write_text(new_text, encoding="utf-8") + + +def write_readme_tag(current_tag: str, new_tag: str) -> None: + text = README_FILE.read_text(encoding="utf-8") + pattern = re.compile(README_DEP_PREFIX + re.escape(current_tag)) + new_text, count = pattern.subn(rf"\g<1>{new_tag}", text, count=1) + if count != 1: + raise SystemExit(f"framework dependency example not updated in {README_FILE}") + README_FILE.write_text(new_text, encoding="utf-8") + + +def ensure_clean_tree() -> None: + result = run(["git", "status", "--porcelain"], capture_output=True) + if result.stdout.strip(): + raise SystemExit("working tree is not clean") + + +def ensure_branch() -> str: + result = run(["git", "symbolic-ref", "--quiet", "--short", "HEAD"], capture_output=True) + branch = result.stdout.strip() + if not branch: + raise SystemExit("release requires a branch checkout") + return branch + + +def ensure_tag_absent(tag: str) -> None: + result = subprocess.run( + ["git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"], + cwd=ROOT, + text=True, + capture_output=True, + ) + if result.returncode == 0: + raise SystemExit(f"tag already exists: {tag}") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Release the iTi-Flask framework") + parser.add_argument( + "version", + nargs="?", + help="target version, for example 0.2.1 or v0.2.1; defaults to patch bump", + ) + args = parser.parse_args() + + current_version = read_current_version() + target_version = normalize_version(args.version) if args.version else bump_patch(current_version) + target_tag = f"v{target_version}" + current_tag = f"v{current_version}" + + if target_version == current_version: + raise SystemExit(f"version already set to {target_version}") + if version_tuple(target_version) <= version_tuple(current_version): + raise SystemExit(f"target version must be newer than {current_version}") + + ensure_clean_tree() + ensure_branch() + ensure_tag_absent(target_tag) + run(["uv", "run", "pytest", "-q"]) + + write_about_version(target_version) + write_copier_tag(current_tag, target_tag) + write_readme_tag(current_tag, target_tag) + + run(["git", "add", "iti/__about__.py", "copier.yml", "README.md"]) + run(["git", "commit", "-m", f"chore: release {target_tag}"]) + run(["git", "tag", "-a", target_tag, "-m", f"release {target_tag}"]) + + branch = ensure_branch() + run(["git", "push", "origin", branch, target_tag]) + + print(f"released {target_tag}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())