diff --git a/README.md b/README.md index c82c67a..304cac4 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,13 @@ AI 修改本项目时优先读: ```toml dependencies = [ +<<<<<<< HEAD "iti-flask @ git+ssh://git@example.com/iTi-Flask.git@v0.2.0", "iti-system @ git+ssh://git@example.com/iTi-System.git@v0.2.1", +======= + "iti-flask @ git+https://git.noahlan.cn/iti-framework/iTi-Flask.git@v0.2.2", + "iti-system @ git+https://git.noahlan.cn/iti-framework/iTi-System.git@v0.2.2", +>>>>>>> 9c8246e (docs: docs更新) ] ``` @@ -97,5 +102,5 @@ uv run pytest -q ```bash ./scripts/iti-system.sh release -./scripts/iti-system.sh release v0.2.1 +./scripts/iti-system.sh release v0.2.2 ``` diff --git a/iti_system/__about__.py b/iti_system/__about__.py index 5eb93a5..ce6aeaf 100644 --- a/iti_system/__about__.py +++ b/iti_system/__about__.py @@ -1,4 +1,8 @@ # SPDX-FileCopyrightText: 2025-present NoahLan <6995syu@163.com> # # SPDX-License-Identifier: MIT +<<<<<<< HEAD __version__ = "0.2.1" +======= +__version__ = "0.2.2" +>>>>>>> 9c8246e (docs: docs更新) diff --git a/pyproject.toml b/pyproject.toml index 4adfb2d..0a3f08d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ iti_system = [ ] [tool.uv.sources] -iti-flask = { path = "../iTi-Flask", editable = true } +iti-flask = { git = "https://git.noahlan.cn/iti-framework/iTi-Flask.git", tag = "v0.2.2" } [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/scripts/iti-system.sh b/scripts/iti-system.sh index f0048bb..1d59ddd 100755 --- a/scripts/iti-system.sh +++ b/scripts/iti-system.sh @@ -42,7 +42,7 @@ case "$command" in uv run pytest -q ;; release) - uv run python scripts/release.py "$@" + sh scripts/release.sh "$@" ;; *) echo "未知命令:$command" >&2 diff --git a/scripts/release.py b/scripts/release.py deleted file mode 100644 index 1563f7d..0000000 --- a/scripts/release.py +++ /dev/null @@ -1,135 +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_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()) diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..3ea98a4 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env sh +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$ROOT_DIR" + +ABOUT_FILE="$ROOT_DIR/iti_system/__about__.py" +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_readme_tags() { + current_version=$1 + target_version=$2 + old_line=' "iti-system @ git+https://git.noahlan.cn/iti-framework/iTi-System.git@v'"$current_version"'",' + new_line=' "iti-system @ git+https://git.noahlan.cn/iti-framework/iTi-System.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_readme_tags "$current_version" "$target_version" + + git add iti_system/__about__.py 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 "$@"