#!/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()