scripts/release.py:
- --patch/--minor/--major flag bumps VERSION file
- Renames [Unreleased] → [x.y.z] — date in CHANGELOG.md
- Inserts fresh [Unreleased] section + correct compare links
- Commits + creates annotated git tag
- --dry-run flag for preview without writes
- Prints git push + GitHub Releases URL on completion
scripts/hooks/pre-commit:
- Warns (exit 0, non-blocking) when source files are staged but
CHANGELOG.md or VERSION are not staged
- Already installed in .git/hooks/
scripts/install-hooks.sh:
- One-liner to install hooks after a fresh clone
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
5.1 KiB
Python
Executable File
148 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
release.py — bump version, promote [Unreleased] in CHANGELOG, commit, tag.
|
|
|
|
Usage:
|
|
python scripts/release.py --patch # 1.1.0 → 1.1.1
|
|
python scripts/release.py --minor # 1.1.0 → 1.2.0
|
|
python scripts/release.py --major # 1.1.0 → 2.0.0
|
|
python scripts/release.py --dry-run --minor # preview only, no writes
|
|
|
|
What it does:
|
|
1. Reads current version from VERSION
|
|
2. Computes the next version
|
|
3. In CHANGELOG.md: renames "## [Unreleased]" → "## [x.y.z] — YYYY-MM-DD"
|
|
and inserts a fresh "## [Unreleased]" + compare link block at the top
|
|
4. Writes new version to VERSION
|
|
5. git add VERSION CHANGELOG.md
|
|
6. git commit -m "Release vX.Y.Z"
|
|
7. git tag -a vX.Y.Z -m "vX.Y.Z"
|
|
8. Prints push instructions
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent
|
|
VERSION_FILE = REPO_ROOT / "VERSION"
|
|
CHANGELOG_FILE = REPO_ROOT / "CHANGELOG.md"
|
|
GITHUB_REPO = "mARTin-B78/tts-voice-creator-clone-and-design-2"
|
|
GITHUB_BASE = f"https://github.com/{GITHUB_REPO}"
|
|
|
|
|
|
def read_version() -> tuple[int, int, int]:
|
|
text = VERSION_FILE.read_text().strip()
|
|
parts = text.split(".")
|
|
if len(parts) != 3:
|
|
sys.exit(f"Invalid VERSION file content: {text!r}. Expected MAJOR.MINOR.PATCH")
|
|
return int(parts[0]), int(parts[1]), int(parts[2])
|
|
|
|
|
|
def bump(current: tuple[int, int, int], kind: str) -> tuple[int, int, int]:
|
|
major, minor, patch = current
|
|
if kind == "major":
|
|
return (major + 1, 0, 0)
|
|
if kind == "minor":
|
|
return (major, minor + 1, 0)
|
|
return (major, minor, patch + 1)
|
|
|
|
|
|
def fmt_version(v: tuple[int, int, int]) -> str:
|
|
return ".".join(map(str, v))
|
|
|
|
|
|
def git(*args: str, check: bool = True) -> str:
|
|
result = subprocess.run(["git", *args], capture_output=True, text=True, cwd=REPO_ROOT)
|
|
if check and result.returncode != 0:
|
|
sys.exit(f"git {' '.join(args)} failed:\n{result.stderr.strip()}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Bump version and promote CHANGELOG")
|
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
group.add_argument("--patch", action="store_true")
|
|
group.add_argument("--minor", action="store_true")
|
|
group.add_argument("--major", action="store_true")
|
|
parser.add_argument("--dry-run", action="store_true", help="Preview only, no file writes or git ops")
|
|
args = parser.parse_args()
|
|
|
|
kind = "patch" if args.patch else "minor" if args.minor else "major"
|
|
dry = args.dry_run
|
|
|
|
current = read_version()
|
|
next_ver = bump(current, kind)
|
|
cur_str = fmt_version(current)
|
|
new_str = fmt_version(next_ver)
|
|
today = date.today().isoformat()
|
|
|
|
print(f" Current version : {cur_str}")
|
|
print(f" Next version : {new_str} ({kind} bump)")
|
|
print(f" Release date : {today}")
|
|
print(f" Dry run : {dry}")
|
|
print()
|
|
|
|
# ── Patch CHANGELOG.md ────────────────────────────────────────────────────
|
|
changelog = CHANGELOG_FILE.read_text()
|
|
|
|
# Must have an [Unreleased] section
|
|
if "## [Unreleased]" not in changelog:
|
|
sys.exit("CHANGELOG.md has no [Unreleased] section. Add one before releasing.")
|
|
|
|
# Build the new compare links block
|
|
link_unreleased = f"[Unreleased]: {GITHUB_BASE}/compare/v{new_str}...HEAD"
|
|
link_new = f"[{new_str}]: {GITHUB_BASE}/compare/v{cur_str}...v{new_str}"
|
|
|
|
# Replace the existing [Unreleased] compare link if present, else append
|
|
if "[Unreleased]:" in changelog:
|
|
changelog = re.sub(
|
|
r"^\[Unreleased\]:.*$",
|
|
f"{link_unreleased}\n{link_new}",
|
|
changelog,
|
|
flags=re.MULTILINE,
|
|
)
|
|
else:
|
|
changelog = changelog.rstrip() + f"\n{link_unreleased}\n{link_new}\n"
|
|
|
|
# Rename "## [Unreleased]" → "## [x.y.z] — date"
|
|
new_version_header = f"## [{new_str}] — {today}"
|
|
changelog = changelog.replace(
|
|
"## [Unreleased]",
|
|
f"## [Unreleased]\n\n---\n\n{new_version_header}",
|
|
1, # only first occurrence
|
|
)
|
|
|
|
print(" CHANGELOG.md changes:")
|
|
for line in changelog.splitlines():
|
|
if new_str in line or "Unreleased" in line:
|
|
print(f" {line}")
|
|
print()
|
|
|
|
if not dry:
|
|
CHANGELOG_FILE.write_text(changelog)
|
|
VERSION_FILE.write_text(new_str + "\n")
|
|
print(f" ✓ Wrote VERSION → {new_str}")
|
|
print(f" ✓ Wrote CHANGELOG.md")
|
|
|
|
git("add", "VERSION", "CHANGELOG.md")
|
|
git("commit", "--no-verify", "-m", f"Release v{new_str}")
|
|
git("tag", "-a", f"v{new_str}", "-m", f"v{new_str}")
|
|
|
|
print(f"\n ✓ Committed and tagged v{new_str}")
|
|
print(f"\n Push with:")
|
|
print(f" git push origin main --tags")
|
|
print(f"\n Then create a GitHub Release at:")
|
|
print(f" {GITHUB_BASE}/releases/new?tag=v{new_str}")
|
|
else:
|
|
print(" [dry-run] No files written, no git operations performed.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|