chore: v0.2.2

main
NoahLan 1 week ago
parent 241f1d9575
commit 761446665d

@ -59,6 +59,8 @@ iTi-Flask 是 FastAPI 后端框架基座。
- 生成项目必须保留 `.copier-answers.yml`,否则不能用 `copier update` 同步模板。 - 生成项目必须保留 `.copier-answers.yml`,否则不能用 `copier update` 同步模板。
- 已生成项目同步框架依赖用 `./app.sh framework-sync` - 已生成项目同步框架依赖用 `./app.sh framework-sync`
- 已生成项目检查和同步模板用 `./app.sh template-check`、`./app.sh template-update`。 - 已生成项目检查和同步模板用 `./app.sh template-check`、`./app.sh template-update`。
- 模板项目的 Alembic 命令必须显式使用 `-c migrations/alembic.ini`
- 模板项目的测试命令使用 `uv run --extra dev pytest -q`,避免未安装 dev extra 时找不到 pytest。
## 命令 ## 命令

@ -45,7 +45,7 @@ scripts\iti.cmd install
```toml ```toml
dependencies = [ dependencies = [
"iti-flask @ git+ssh://git@your-git/iTi/iTi-Flask.git@v0.2.1", "iti-flask @ git+https://git.noahlan.cn/iti-framework/iTi-Flask.git@v0.2.2",
] ]
``` ```
@ -107,7 +107,14 @@ cd ../my-system-app
```bash ```bash
./scripts/iti.sh release ./scripts/iti.sh release
./scripts/iti.sh release v0.2.1 ./scripts/iti.sh release v0.2.2
```
Windows:
```bat
scripts\iti.cmd release
scripts\iti.cmd release v0.2.2
``` ```
## 文档 ## 文档

@ -4,6 +4,7 @@ __pycache__/
.hatch/ .hatch/
.pytest_cache/ .pytest_cache/
.mypy_cache/ .mypy_cache/
*.egg-info/
.coverage .coverage
htmlcov/ htmlcov/
*.db *.db

@ -47,10 +47,18 @@ app.cmd init-system
## 开发 ## 开发
```bash ```bash
./app.sh serve 8000 ./app.sh serve
./app.sh test ./app.sh test
``` ```
不同环境直接在命令前设 `APP_ENV`,或把环境名作为 `serve` 的第一个参数:
```bash
./app.sh serve
./app.sh serve test 8000
APP_ENV=prod ./app.sh migrate
```
## 同步更新 ## 同步更新
同步框架包: 同步框架包:
@ -86,4 +94,4 @@ app.cmd init-system
- `migrations/versions` 必须提交。 - `migrations/versions` 必须提交。
- migration message 第一个词写作者名。 - migration message 第一个词写作者名。
- 生产只从 `main` 执行 `alembic upgrade head`。 - 生产只从 `main` 执行 `alembic -c migrations/alembic.ini upgrade head`。

@ -4,6 +4,7 @@ setlocal enabledelayedexpansion
set "SCRIPT_DIR=%~dp0" set "SCRIPT_DIR=%~dp0"
set "ROOT_DIR=%SCRIPT_DIR%" set "ROOT_DIR=%SCRIPT_DIR%"
pushd "%ROOT_DIR%" >nul pushd "%ROOT_DIR%" >nul
set "UV_NO_SOURCES_PACKAGE=iti-flask"
set "PROJECT_VENV=%ROOT_DIR%.venv" set "PROJECT_VENV=%ROOT_DIR%.venv"
if defined VIRTUAL_ENV ( if defined VIRTUAL_ENV (
@ -50,8 +51,8 @@ echo install 安装开发依赖uv sync --extra dev
echo framework-sync 同步 iTi-Flask{% if include_system %} / iTi-System{% endif %} 依赖 echo framework-sync 同步 iTi-Flask{% if include_system %} / iTi-System{% endif %} 依赖
echo template-check 检查 Copier 模板是否有更新 echo template-check 检查 Copier 模板是否有更新
echo template-update 按 Copier 模板更新项目骨架 echo template-update 按 Copier 模板更新项目骨架
echo test 运行测试uv run pytest -q echo test 运行测试uv run --extra dev pytest -q
echo serve [端口] 本地启动,默认 8000 echo serve [环境] [端口] 本地启动,默认 dev / 8000
echo migrate 执行 Alembic upgrade head echo migrate 执行 Alembic upgrade head
echo migration ^<说明^> 生成 migration说明建议以作者名开头 echo migration ^<说明^> 生成 migration说明建议以作者名开头
echo heads 查看 Alembic heads echo heads 查看 Alembic heads
@ -63,7 +64,9 @@ echo init-system system-sync + migrate + system-seed
echo. echo.
echo 示例: echo 示例:
echo app.cmd install echo app.cmd install
echo app.cmd serve 8000 echo app.cmd serve
echo app.cmd serve test 8000
echo set APP_ENV=prod ^&^& app.cmd migrate
echo app.cmd migration "alice add order table" echo app.cmd migration "alice add order table"
{% if include_system %}echo app.cmd init-system {% if include_system %}echo app.cmd init-system
{% endif %}popd >nul {% endif %}popd >nul
@ -90,17 +93,44 @@ uvx copier update --defaults --vcs-ref HEAD "%ROOT_DIR%"
goto end goto end
:test :test
uv run pytest -q uv run --extra dev pytest -q
goto end goto end
:serve :serve
set "PORT=%~1" set "ENV_NAME=%APP_ENV%"
if not defined ENV_NAME set "ENV_NAME=%ITI_ENV%"
if not defined ENV_NAME set "ENV_NAME=dev"
set "PORT=8000"
set "ARG1=%~1"
if /I "%ARG1%"=="dev" (
set "ENV_NAME=dev"
set "PORT=%~2"
) else if /I "%ARG1%"=="test" (
set "ENV_NAME=test"
set "PORT=%~2"
) else if /I "%ARG1%"=="prod" (
set "ENV_NAME=prod"
set "PORT=%~2"
) else if /I "%ARG1%"=="default" (
set "ENV_NAME=dev"
set "PORT=%~2"
) else if not "%ARG1%"=="" (
echo(%ARG1%| findstr /r "^[0-9][0-9]*$" >nul
if errorlevel 1 (
set "ENV_NAME=%ARG1%"
set "PORT=%~2"
) else (
set "PORT=%ARG1%"
)
)
if "%PORT%"=="" set "PORT=8000" if "%PORT%"=="" set "PORT=8000"
set "APP_ENV=%ENV_NAME%"
set "ITI_ENV=%ENV_NAME%"
uv run uvicorn app:app --reload --port "%PORT%" uv run uvicorn app:app --reload --port "%PORT%"
goto end goto end
:migrate :migrate
uv run alembic upgrade head uv run alembic -c migrations/alembic.ini upgrade head
goto end goto end
:migration :migration
@ -109,15 +139,15 @@ if "%MESSAGE%"=="" (
echo 缺少 migration 说明。示例app.cmd migration "alice add order table" 1>&2 echo 缺少 migration 说明。示例app.cmd migration "alice add order table" 1>&2
exit /b 2 exit /b 2
) )
uv run alembic revision --autogenerate -m "%MESSAGE%" uv run alembic -c migrations/alembic.ini revision --autogenerate -m "%MESSAGE%"
goto end goto end
:heads :heads
uv run alembic heads uv run alembic -c migrations/alembic.ini heads
goto end goto end
:current :current
uv run alembic current uv run alembic -c migrations/alembic.ini current
goto end goto end
{% if include_system %}:system_sync {% if include_system %}:system_sync
@ -131,7 +161,7 @@ goto end
:init_system :init_system
uv run iti-system migrations sync --target migrations/versions uv run iti-system migrations sync --target migrations/versions
if errorlevel 1 goto end if errorlevel 1 goto end
uv run alembic upgrade head uv run alembic -c migrations/alembic.ini upgrade head
if errorlevel 1 goto end if errorlevel 1 goto end
uv run iti-system seed system app:app uv run iti-system seed system app:app
goto end goto end
@ -141,7 +171,7 @@ uv sync --extra dev
if errorlevel 1 goto end if errorlevel 1 goto end
{% if include_system %}uv run iti-system migrations sync --target migrations/versions {% if include_system %}uv run iti-system migrations sync --target migrations/versions
if errorlevel 1 goto end if errorlevel 1 goto end
{% endif %}uv run alembic upgrade head {% endif %}uv run alembic -c migrations/alembic.ini upgrade head
if errorlevel 1 goto end if errorlevel 1 goto end
{% if include_system %}uv run iti-system seed system app:app {% if include_system %}uv run iti-system seed system app:app
{% endif %}goto end {% endif %}goto end

@ -1,3 +1,5 @@
import os
from iti import create_app from iti import create_app
{% if include_system %}from iti_system import create_system_module {% if include_system %}from iti_system import create_system_module
@ -13,4 +15,8 @@ modules = [
] ]
app = create_app(config_mapping=config, modules=modules) app = create_app(
config_name=os.getenv("APP_ENV", os.getenv("ITI_ENV", "dev")),
config_mapping=config,
modules=modules,
)

@ -3,6 +3,7 @@ set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
cd "$ROOT_DIR" cd "$ROOT_DIR"
export UV_NO_SOURCES_PACKAGE=iti-flask
PROJECT_VENV="$ROOT_DIR/.venv" PROJECT_VENV="$ROOT_DIR/.venv"
case "${VIRTUAL_ENV:-}" in case "${VIRTUAL_ENV:-}" in
@ -26,8 +27,8 @@ show_help() {
framework-sync 同步 iTi-Flask{% if include_system %} / iTi-System{% endif %} 依赖 framework-sync 同步 iTi-Flask{% if include_system %} / iTi-System{% endif %} 依赖
template-check 检查 Copier 模板是否有更新 template-check 检查 Copier 模板是否有更新
template-update 按 Copier 模板更新项目骨架 template-update 按 Copier 模板更新项目骨架
test 运行测试uv run pytest -q test 运行测试uv run --extra dev pytest -q
serve [端口] 本地启动,默认 8000 serve [环境] [端口] 本地启动,默认 dev / 8000
migrate 执行 Alembic upgrade head migrate 执行 Alembic upgrade head
migration <说明> 生成 migration说明建议以作者名开头 migration <说明> 生成 migration说明建议以作者名开头
heads 查看 Alembic heads heads 查看 Alembic heads
@ -39,7 +40,9 @@ show_help() {
示例: 示例:
./app.sh install ./app.sh install
./app.sh serve 8000 ./app.sh serve
./app.sh serve test 8000
APP_ENV=prod ./app.sh migrate
./app.sh migration "alice add order table" ./app.sh migration "alice add order table"
{% if include_system %} ./app.sh init-system {% if include_system %} ./app.sh init-system
{% endif %} {% endif %}
@ -66,14 +69,33 @@ case "$command" in
uvx copier update --defaults --vcs-ref HEAD "$ROOT_DIR" uvx copier update --defaults --vcs-ref HEAD "$ROOT_DIR"
;; ;;
test) test)
uv run pytest -q uv run --extra dev pytest -q
;; ;;
serve) serve)
port=${1:-8000} env_name=${APP_ENV:-${ITI_ENV:-dev}}
uv run uvicorn app:app --reload --port "$port" port=8000
if [ $# -gt 0 ]; then
case "$1" in
dev|test|prod|default)
env_name=$1
port=${2:-8000}
;;
''|*[!0-9]*)
env_name=$1
port=${2:-8000}
;;
*)
port=$1
;;
esac
fi
if [ "$env_name" = default ]; then
env_name=dev
fi
APP_ENV="$env_name" ITI_ENV="$env_name" uv run uvicorn app:app --reload --port "$port"
;; ;;
migrate) migrate)
uv run alembic upgrade head uv run alembic -c migrations/alembic.ini upgrade head
;; ;;
migration) migration)
message=${1:-} message=${1:-}
@ -81,13 +103,13 @@ case "$command" in
echo "缺少 migration 说明。示例:./app.sh migration \"alice add order table\"" >&2 echo "缺少 migration 说明。示例:./app.sh migration \"alice add order table\"" >&2
exit 2 exit 2
fi fi
uv run alembic revision --autogenerate -m "$message" uv run alembic -c migrations/alembic.ini revision --autogenerate -m "$message"
;; ;;
heads) heads)
uv run alembic heads uv run alembic -c migrations/alembic.ini heads
;; ;;
current) current)
uv run alembic current uv run alembic -c migrations/alembic.ini current
;; ;;
{% if include_system %} system-sync) {% if include_system %} system-sync)
uv run iti-system migrations sync --target migrations/versions uv run iti-system migrations sync --target migrations/versions
@ -97,13 +119,13 @@ case "$command" in
;; ;;
init-system) init-system)
uv run iti-system migrations sync --target migrations/versions uv run iti-system migrations sync --target migrations/versions
uv run alembic upgrade head uv run alembic -c migrations/alembic.ini upgrade head
uv run iti-system seed system app:app uv run iti-system seed system app:app
;; ;;
{% endif %} init) {% endif %} init)
uv sync --extra dev uv sync --extra dev
{% if include_system %} uv run iti-system migrations sync --target migrations/versions {% if include_system %} uv run iti-system migrations sync --target migrations/versions
{% endif %} uv run alembic upgrade head {% endif %} uv run alembic -c migrations/alembic.ini upgrade head
{% if include_system %} uv run iti-system seed system app:app {% if include_system %} uv run iti-system seed system app:app
{% endif %} ;; {% endif %} ;;
*) *)

@ -1,6 +1,12 @@
import os
from pathlib import Path from pathlib import Path
from iti.config import BaseConfig, DevConfig as BaseDevConfig, ProdConfig as BaseProdConfig from iti.config import (
BaseConfig,
DevConfig as BaseDevConfig,
ProdConfig as BaseProdConfig,
)
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
@ -21,11 +27,21 @@ class TestConfig(BaseConfig):
app_name="{{ project_name }}", app_name="{{ project_name }}",
app_env="test", app_env="test",
testing=True, testing=True,
database_url="sqlite+pysqlite:///:memory:", database_url=os.getenv(
"DATABASE_URL",
f"mysql+pymysql://{os.getenv('MYSQL_USER', 'root')}:"
f"{os.getenv('MYSQL_PASSWORD', 'password')}@"
f"{os.getenv('MYSQL_HOST', '127.0.0.1')}:"
f"{os.getenv('MYSQL_PORT', '3306')}/"
f"{os.getenv('MYSQL_DATABASE', 'hsyh_mes_phase2_test')}?charset=utf8mb4",
),
base_dir=BASE_DIR, base_dir=BASE_DIR,
ratelimit_enabled=False, ratelimit_enabled=False,
log_file_enabled=False, log_file_enabled=False,
) )
self.app_name = "{{ project_name }}"
self.base_dir = BASE_DIR
self.log_dir = str(BASE_DIR / "runtime" / "logs")
class ProdConfig(BaseProdConfig): class ProdConfig(BaseProdConfig):

@ -3,8 +3,8 @@
本目录是业务项目唯一的 Alembic migration 流。 本目录是业务项目唯一的 Alembic migration 流。
```bash ```bash
uv run alembic revision --autogenerate -m "alice add example table" uv run alembic -c migrations/alembic.ini revision --autogenerate -m "alice add example table"
uv run alembic upgrade head uv run alembic -c migrations/alembic.ini upgrade head
``` ```
`versions/` 下的 migration 文件必须提交到 Git。 `versions/` 下的 migration 文件必须提交到 Git。

@ -1,11 +1,17 @@
from __future__ import annotations from __future__ import annotations
import os import os
import sys
from logging.config import fileConfig from logging.config import fileConfig
from pathlib import Path
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
ROOT_DIR = Path(__file__).resolve().parents[1]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from config import config as app_config from config import config as app_config
from iti.db import Base from iti.db import Base
from iti.exchange import models as _exchange_models from iti.exchange import models as _exchange_models

@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "{{ project_slug | replace('_', '-') }}" name = "{{ project_slug | replace('_', '-') }}"
version = "0.2.0" version = "0.2.2"
description = "{{ project_name }}" description = "{{ project_name }}"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"iti-flask @ {{ framework_git }}{% if framework_tag %}@{{ framework_tag }}{% endif %}", "iti-flask @ git+{{ framework_git }}{% if framework_tag %}@{{ framework_tag }}{% endif %}",
{% if include_system %} "iti-system @ {{ system_git }}{% if system_tag %}@{{ system_tag }}{% endif %}", {% if include_system %} "iti-system @ git+{{ system_git }}{% if system_tag %}@{{ system_tag }}{% endif %}",
{% endif -%} {% endif -%}
] ]
@ -25,3 +25,8 @@ include = ["{{ project_slug }}*"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
pythonpath = ["."] pythonpath = ["."]
[tool.uv.sources]
iti-flask = { git = "{{ framework_git }}"{% if framework_tag %}, tag = "{{ framework_tag }}"{% endif %} }
{% if include_system %}iti-system = { git = "{{ system_git }}"{% if system_tag %}, tag = "{{ system_tag }}"{% endif %} }
{% endif -%}

@ -21,12 +21,12 @@ project_slug:
framework_git: framework_git:
type: str type: str
help: iTi-Flask Git 地址 help: iTi-Flask Git 地址
default: git+ssh://git@your-git/iTi/iTi-Flask.git default: https://git.noahlan.cn/iti-framework/iTi-Flask.git
framework_tag: framework_tag:
type: str type: str
help: iTi-Flask Git tag help: iTi-Flask Git tag
default: v0.2.1 default: v0.2.2
include_system: include_system:
type: bool type: bool
@ -36,9 +36,9 @@ include_system:
system_git: system_git:
type: str type: str
help: iTi-System Git 地址 help: iTi-System Git 地址
default: git+ssh://git@your-git/iTi/iTi-System.git default: https://git.noahlan.cn/iti-framework/iTi-System.git
system_tag: system_tag:
type: str type: str
help: iTi-System Git tag help: iTi-System Git tag
default: v0.2.0 default: v0.2.2

@ -98,6 +98,8 @@ app.cmd init-system
./app.sh migrate ./app.sh migrate
``` ```
`app.sh` / `app.cmd` 内部使用项目自己的 `migrations/alembic.ini`
`iti-system` 的项目还会有: `iti-system` 的项目还会有:
```bash ```bash

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

@ -111,10 +111,19 @@ uv run alembic current
goto end goto end
:release :release
set "SHELL_CMD="
where sh >nul 2>nul && set "SHELL_CMD=sh"
if not defined SHELL_CMD (
where bash >nul 2>nul && set "SHELL_CMD=bash"
)
if not defined SHELL_CMD (
echo 找不到 sh 或 bash无法运行 release 脚本 1>&2
exit /b 1
)
if "%~1"=="" ( if "%~1"=="" (
uv run python scripts/release.py "%SHELL_CMD%" scripts/release.sh
) else ( ) else (
uv run python scripts/release.py %* "%SHELL_CMD%" scripts/release.sh %*
) )
goto end goto end

@ -86,7 +86,7 @@ case "$command" in
uv run alembic current uv run alembic current
;; ;;
release) release)
uv run python scripts/release.py "$@" sh scripts/release.sh "$@"
;; ;;
make-app|make-system-app) make-app|make-system-app)
target=${1:-} target=${1:-}

@ -1,146 +0,0 @@
#!/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())

@ -0,0 +1,202 @@
#!/usr/bin/env sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$ROOT_DIR"
ABOUT_FILE="$ROOT_DIR/iti/__about__.py"
COPIER_FILE="$ROOT_DIR/copier.yml"
README_FILE="$ROOT_DIR/README.md"
die() {
printf '%s\n' "$*" >&2
exit 1
}
parse_version() {
raw=$1
version=${raw#v}
old_ifs=$IFS
IFS=.
set -- $version
IFS=$old_ifs
if [ "$#" -ne 3 ]; then
die "invalid version: $raw"
fi
for part in "$@"; do
case $part in
''|*[!0-9]*)
die "invalid version: $raw"
;;
esac
done
printf '%s %s %s\n' "$1" "$2" "$3"
}
normalize_version() {
set -- $(parse_version "$1")
printf '%s.%s.%s\n' "$1" "$2" "$3"
}
bump_patch() {
set -- $(parse_version "$1")
printf '%s.%s.%s\n' "$1" "$2" "$(( $3 + 1 ))"
}
version_is_newer() {
current_parts=$(parse_version "$1")
set -- $current_parts
current_major=$1
current_minor=$2
current_patch=$3
target_parts=$(parse_version "$2")
set -- $target_parts
target_major=$1
target_minor=$2
target_patch=$3
if [ "$target_major" -gt "$current_major" ]; then
return 0
fi
if [ "$target_major" -lt "$current_major" ]; then
return 1
fi
if [ "$target_minor" -gt "$current_minor" ]; then
return 0
fi
if [ "$target_minor" -lt "$current_minor" ]; then
return 1
fi
[ "$target_patch" -gt "$current_patch" ]
}
read_current_version() {
current_version=$(sed -n 's/^__version__[[:space:]]*=[[:space:]]*"\([^"]*\)".*$/\1/p' "$ABOUT_FILE" | sed -n '1p')
[ -n "$current_version" ] || die "version not found in $ABOUT_FILE"
printf '%s\n' "$current_version"
}
replace_first_exact_line() {
file=$1
old_line=$2
new_line=$3
tmp_file=$(mktemp)
if awk -v old="$old_line" -v new="$new_line" '
BEGIN { done = 0 }
{
if (!done && $0 == old) {
print new
done = 1
} else {
print
}
}
END { if (!done) exit 1 }
' "$file" >"$tmp_file"; then
mv "$tmp_file" "$file"
else
rm -f "$tmp_file"
die "line not updated in $file"
fi
}
write_about_version() {
current_version=$1
target_version=$2
replace_first_exact_line "$ABOUT_FILE" "__version__ = \"$current_version\"" "__version__ = \"$target_version\""
}
write_copier_tag() {
current_version=$1
target_version=$2
tmp_file=$(mktemp)
if awk -v current="$current_version" -v target="$target_version" '
BEGIN { in_framework = 0; done = 0 }
/^framework_tag:[[:space:]]*$/ { in_framework = 1; print; next }
in_framework && /^[^[:space:]]/ { in_framework = 0 }
in_framework && !done && $0 == " default: v" current {
print " default: v" target
done = 1
next
}
{ print }
END { if (!done) exit 1 }
' "$COPIER_FILE" >"$tmp_file"; then
mv "$tmp_file" "$COPIER_FILE"
else
rm -f "$tmp_file"
die "framework_tag default not updated in $COPIER_FILE"
fi
}
write_readme_tag() {
current_version=$1
target_version=$2
old_line=' "iti-flask @ git+https://git.noahlan.cn/iti-framework/iTi-Flask.git@v'"$current_version"'",'
new_line=' "iti-flask @ git+https://git.noahlan.cn/iti-framework/iTi-Flask.git@v'"$target_version"'",'
replace_first_exact_line "$README_FILE" "$old_line" "$new_line"
}
ensure_clean_tree() {
if [ -n "$(git status --porcelain)" ]; then
die "working tree is not clean"
fi
}
ensure_branch() {
if branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null); then
[ -n "$branch" ] || die "release requires a branch checkout"
printf '%s\n' "$branch"
else
die "release requires a branch checkout"
fi
}
ensure_tag_absent() {
tag=$1
if git rev-parse --verify --quiet "refs/tags/$tag" >/dev/null; then
die "tag already exists: $tag"
fi
}
main() {
version_arg=${1:-}
current_version=$(read_current_version)
if [ -n "$version_arg" ]; then
target_version=$(normalize_version "$version_arg")
else
target_version=$(bump_patch "$current_version")
fi
target_tag="v$target_version"
if [ "$target_version" = "$current_version" ]; then
die "version already set to $target_version"
fi
if ! version_is_newer "$current_version" "$target_version"; then
die "target version must be newer than $current_version"
fi
ensure_clean_tree
branch=$(ensure_branch)
ensure_tag_absent "$target_tag"
uv run pytest -q
write_about_version "$current_version" "$target_version"
write_copier_tag "$current_version" "$target_version"
write_readme_tag "$current_version" "$target_version"
git add iti/__about__.py copier.yml README.md
git commit -m "chore: release $target_tag"
git tag -a "$target_tag" -m "release $target_tag"
git push origin "$branch" "$target_tag"
printf 'released %s\n' "$target_tag"
}
main "$@"
Loading…
Cancel
Save