From 06ed33ca672de78829907355816c0f3a0ed8a6ff Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 29 May 2026 14:14:41 +0200 Subject: [PATCH] Add release automation: version bump script and pre-commit changelog hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 12 ++++ scripts/hooks/pre-commit | 31 +++++++++ scripts/install-hooks.sh | 10 +++ scripts/release.py | 147 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100755 scripts/hooks/pre-commit create mode 100644 scripts/install-hooks.sh create mode 100755 scripts/release.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7259799..274bd5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ## [Unreleased] +### Added + +- **`scripts/release.py`** — automates version bump + CHANGELOG promotion. + `python scripts/release.py --patch|--minor|--major [--dry-run]` renames + `[Unreleased]` to the new version, updates compare links, writes `VERSION`, + commits, and creates an annotated git tag in one command. +- **Git pre-commit hook** (`scripts/hooks/pre-commit`) — warns (does not block) + when `.py`/`.js`/`.css`/`.html` files are staged but `CHANGELOG.md` or + `VERSION` are not. Run `bash scripts/install-hooks.sh` after cloning. +- **`scripts/install-hooks.sh`** — one-liner to install the hook after a fresh + clone: `bash scripts/install-hooks.sh`. + --- ## [1.1.0] — 2026-05-29 diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 0000000..64497d3 --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Pre-commit: warn if CHANGELOG.md and VERSION are not staged. +# Never exits 1 — this is a reminder, not a blocker. + +staged=$(git diff --cached --name-only 2>/dev/null) + +# Only check when real source files are being committed (ignore pure docs/assets) +source_staged=$(echo "$staged" | grep -E '\.(py|js|html|css|yml|yaml|json)$' | head -1) +if [ -z "$source_staged" ]; then + exit 0 +fi + +missing=() +echo "$staged" | grep -q "CHANGELOG.md" || missing+=("CHANGELOG.md") +echo "$staged" | grep -q "^VERSION$" || missing+=("VERSION") + +if [ ${#missing[@]} -gt 0 ]; then + echo "" + echo " ⚠️ Reminder: the following files were not staged:" + for f in "${missing[@]}"; do + echo " - $f" + done + echo "" + echo " Update CHANGELOG.md ([Unreleased] section) and bump VERSION if" + echo " this commit introduces a user-visible change." + echo " Run python scripts/release.py --minor to automate a release." + echo " Skip with git commit --no-verify if this is an internal-only change." + echo "" +fi + +exit 0 diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100644 index 0000000..cfa6b41 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Run once after cloning: bash scripts/install-hooks.sh +# Copies the project's git hooks into .git/hooks/ +set -e +REPO_ROOT="$(git rev-parse --show-toplevel)" +HOOKS_SRC="$REPO_ROOT/scripts/hooks" +HOOKS_DST="$REPO_ROOT/.git/hooks" +cp "$HOOKS_SRC/pre-commit" "$HOOKS_DST/pre-commit" +chmod +x "$HOOKS_DST/pre-commit" +echo "✓ Installed pre-commit hook" diff --git a/scripts/release.py b/scripts/release.py new file mode 100755 index 0000000..e2daf0f --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,147 @@ +#!/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()