chore: add independent release workflow

main
NoahLan 2 weeks ago
parent 33d3b171ab
commit 26c97cdc5b

@ -89,3 +89,13 @@ seed 会写入默认角色、默认管理员、系统菜单、系统配置、系
uv sync --extra dev uv sync --extra dev
uv run pytest -q uv run pytest -q
``` ```
## 发布
`iTi-System` 独立发布。
它不跟随 `iTi-Flask` 自动发版。
```bash
./scripts/iti-system.sh release
./scripts/iti-system.sh release v0.2.1
```

@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2025-present NoahLan <6995syu@163.com>
#
# SPDX-License-Identifier: MIT
__version__ = "0.2.0"

@ -1,6 +1,19 @@
from .module import SystemModule, create_system_module from .__about__ import __version__
__all__ = [ __all__ = [
"SystemModule", "SystemModule",
"__version__",
"create_system_module", "create_system_module",
] ]
def __getattr__(name: str):
if name in {"SystemModule", "create_system_module"}:
from .module import SystemModule, create_system_module
values = {
"SystemModule": SystemModule,
"create_system_module": create_system_module,
}
return values[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

@ -7,8 +7,11 @@ from pathlib import Path
import click import click
from .__about__ import __version__
@click.group() @click.group()
@click.version_option(__version__, prog_name="iti-system")
def iti_system_cli() -> None: def iti_system_cli() -> None:
"""iTi-System commands.""" """iTi-System commands."""

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "iti-system" name = "iti-system"
version = "0.2.0" dynamic = ["version"]
description = "Reusable system-domain package for iTi-Flask applications." description = "Reusable system-domain package for iTi-Flask applications."
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
@ -26,6 +26,9 @@ dev = [
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["iti_system*"] include = ["iti_system*"]
[tool.setuptools.dynamic]
version = { attr = "iti_system.__about__.__version__" }
[tool.setuptools.package-data] [tool.setuptools.package-data]
iti_system = [ iti_system = [
"migrations/versions/*.py", "migrations/versions/*.py",

@ -0,0 +1,53 @@
#!/usr/bin/env sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$ROOT_DIR"
PROJECT_VENV="$ROOT_DIR/.venv"
case "${VIRTUAL_ENV:-}" in
""|"$PROJECT_VENV"|"$PROJECT_VENV/")
;;
*)
unset VIRTUAL_ENV
;;
esac
show_help() {
cat <<'EOF'
iTi-System 开发脚本
用法:
./scripts/iti-system.sh <命令> [参数]
常用命令:
help 显示帮助
install 安装开发依赖uv sync --extra dev
test 运行测试uv run pytest -q
release [版本] 发布系统包:测试、改版本、提交、打 tag、推送
EOF
}
command=${1:-help}
shift || true
case "$command" in
help|-h|--help)
show_help
;;
install)
uv sync --extra dev
;;
test)
uv run pytest -q
;;
release)
uv run python scripts/release.py "$@"
;;
*)
echo "未知命令:$command" >&2
echo >&2
show_help >&2
exit 2
;;
esac

@ -0,0 +1,135 @@
#!/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_system" / "__about__.py"
README_FILE = ROOT / "README.md"
VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
ABOUT_VERSION_RE = re.compile(r'(__version__\s*=\s*")([^"]+)(")')
README_DEP_RE = re.compile(
r"(iti-system @ git\+ssh://git@example\.com/iTi-System\.git@)v\d+\.\d+\.\d+"
)
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_readme_tags(version: str) -> None:
text = README_FILE.read_text(encoding="utf-8")
new_text, count = README_DEP_RE.subn(rf"\g<1>v{version}", text)
if count != 1:
raise SystemExit(f"dependency examples 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-System package")
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}"
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_readme_tags(target_version)
run(["git", "add", "iti_system/__about__.py", "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())

@ -1,6 +1,9 @@
from __future__ import annotations from __future__ import annotations
from importlib.metadata import version
from iti_system import SystemModule, create_system_module from iti_system import SystemModule, create_system_module
from iti_system.__about__ import __version__
def test_create_system_module_returns_full_system_module(): def test_create_system_module_returns_full_system_module():
@ -8,3 +11,7 @@ def test_create_system_module_returns_full_system_module():
assert isinstance(module, SystemModule) assert isinstance(module, SystemModule)
assert module.name == "iti_system" assert module.name == "iti_system"
def test_package_exports_version():
assert __version__ == version("iti-system")

Loading…
Cancel
Save