chore: v0.2.2
parent
241f1d9575
commit
761446665d
@ -1,11 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from logging.config import fileConfig
|
||||
from pathlib import Path
|
||||
|
||||
from alembic import context
|
||||
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 iti.db import Base
|
||||
from iti.exchange import models as _exchange_models
|
||||
@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2025-present NoahLan <6995syu@163.com>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
__version__ = "0.2.1"
|
||||
__version__ = "0.2.2"
|
||||
|
||||
@ -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…
Reference in New Issue