tts-voice-creator-clone-and.../scripts/release.py
mARTin-B78 03597d37f0
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
fix(release): keep index.html's cache-busting version tags in sync
static/index.html hardcodes ?v= query strings on style.css, the per-page
CSS files and loader.js, plus an app-version meta tag. Nothing kept them
in sync with VERSION, so they'd silently drifted to different stale
versions each (found live: style.css one release behind, loader.js
sixteen releases behind). The static-file middleware marks any CSS/JS
request with a ?v= query as max-age=31536000 immutable, so a browser
that had ever loaded e.g. magazine.js?v=1.26.10 kept serving that exact
response for a year no matter how many times the file changed on disk
or how hard the page was force-reloaded.

release.py now rewrites every ?v=X.Y.Z and the app-version meta tag to
the new version on every release, so a release is what invalidates the
cache rather than a manual, easy-to-forget edit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:28:46 +02:00

174 lines
6.5 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"
INDEX_HTML_FILE = REPO_ROOT / "static" / "index.html"
GITHUB_REPO = "mARTin-B78/tts-voice-creator-clone-and-design-2"
GITHUB_BASE = f"https://github.com/{GITHUB_REPO}"
# static/index.html hardcodes ?v= cache-busting query strings on style.css,
# the per-page CSS files and loader.js, plus an <meta name="app-version">
# tag. Nothing previously kept these in sync with VERSION, so they silently
# drifted to different stale versions each (seen live: style.css one release
# behind, loader.js sixteen releases behind) -- StaticCacheHeadersMiddleware
# in server.py marks any *.css/js request WITH a ?v= query
# "Cache-Control: public, max-age=31536000, immutable", so once a browser
# had fetched e.g. magazine.js?v=1.26.10 it kept serving that exact response
# for a year regardless of how many times the file changed on disk or how
# hard the page was reloaded. Rewriting every ?v=X.Y.Z to the new version on
# every release is what actually invalidates that cache.
_VERSION_QUERY_RE = re.compile(r"(\?v=)\d+\.\d+\.\d+")
_APP_VERSION_META_RE = re.compile(r'(<meta name="app-version" content=")[^"]*(")')
def bump_index_html(new_str: str) -> str:
html = INDEX_HTML_FILE.read_text()
html = _VERSION_QUERY_RE.sub(rf"\g<1>{new_str}", html)
html = _APP_VERSION_META_RE.sub(rf"\g<1>{new_str}\g<2>", html)
return html
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()
index_html = bump_index_html(new_str)
if not dry:
CHANGELOG_FILE.write_text(changelog)
VERSION_FILE.write_text(new_str + "\n")
INDEX_HTML_FILE.write_text(index_html)
print(f" ✓ Wrote VERSION → {new_str}")
print(f" ✓ Wrote CHANGELOG.md")
print(f" ✓ Wrote static/index.html cache-busting tags → {new_str}")
git("add", "VERSION", "CHANGELOG.md", "static/index.html")
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()