chore: add independent release workflow
parent
33d3b171ab
commit
26c97cdc5b
@ -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__ = [
|
||||
"SystemModule",
|
||||
"__version__",
|
||||
"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}")
|
||||
|
||||
@ -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())
|
||||
Loading…
Reference in New Issue