|
| 1 | +"""Release Tagging Helper |
| 2 | +
|
| 3 | +Automates version bump, changelog scaffold, release note creation. |
| 4 | +Usage: |
| 5 | + python scripts/tag_release.py --version 0.1.1-testnet --title "visual polish" --preview |
| 6 | + python scripts/tag_release.py --version 0.1.1-testnet --title "visual polish" --apply |
| 7 | +
|
| 8 | +Requires: clean working tree; run via PR process before tagging. |
| 9 | +""" |
| 10 | + |
| 11 | +from __future__ import annotations |
| 12 | +import argparse, re, sys, datetime, pathlib |
| 13 | + |
| 14 | +ROOT = pathlib.Path(__file__).resolve().parent.parent |
| 15 | +VERSION_FILE = ROOT / "src" / "VERSION.py" |
| 16 | +CHANGELOG = ROOT / "CHANGELOG.md" |
| 17 | +RELEASE_DIR = ROOT / "docs" |
| 18 | + |
| 19 | +def read_version_py() -> str: |
| 20 | + text = VERSION_FILE.read_text(encoding="utf-8") |
| 21 | + m = re.search(r'__version__\s*=\s*"([^"]+)"', text) |
| 22 | + return m.group(1) if m else "UNKNOWN" |
| 23 | + |
| 24 | +def update_version_py(new_version: str, apply: bool): |
| 25 | + text = VERSION_FILE.read_text(encoding="utf-8") |
| 26 | + text = re.sub(r'__version__\s*=\s*"[^"]+"', f'__version__ = "{new_version}"', text) |
| 27 | + text = re.sub(r'__date__\s*=\s*"[^"]+"', f'__date__ = "{datetime.date.today()}"', text) |
| 28 | + if apply: |
| 29 | + VERSION_FILE.write_text(text, encoding="utf-8") |
| 30 | + return text |
| 31 | + |
| 32 | +def append_changelog(new_version: str, title: str, apply: bool): |
| 33 | + entry = ( |
| 34 | + f"## [{new_version}] - {datetime.date.today()}\n\n" |
| 35 | + f"### Pending\n" |
| 36 | + f"- Placeholder: {title}\n" |
| 37 | + f"- Fill in merged items before tag.\n\n" |
| 38 | + ) |
| 39 | + original = CHANGELOG.read_text(encoding="utf-8") |
| 40 | + updated = entry + original |
| 41 | + if apply: |
| 42 | + CHANGELOG.write_text(updated, encoding="utf-8") |
| 43 | + return entry |
| 44 | + |
| 45 | +def create_release_note(new_version: str, title: str, apply: bool): |
| 46 | + fname = RELEASE_DIR / f"RELEASE_NOTE_{new_version.replace('.', '_').upper()}.md" |
| 47 | + content = ( |
| 48 | + f"# Release {new_version}\n\n" |
| 49 | + f"## Draft - {title}\n\n" |
| 50 | + "Populate after merging PRs.\n\n" |
| 51 | + "## Checklist\n" |
| 52 | + "- [ ] Changelog finalized\n" |
| 53 | + "- [ ] Version bump committed\n" |
| 54 | + "- [ ] Security scan re-run\n" |
| 55 | + "- [ ] Feature flags validated\n" |
| 56 | + "- [ ] Tag annotated & pushed\n" |
| 57 | + ) |
| 58 | + if apply: |
| 59 | + fname.write_text(content, encoding="utf-8") |
| 60 | + return fname, content |
| 61 | + |
| 62 | +def main(): |
| 63 | + p = argparse.ArgumentParser() |
| 64 | + p.add_argument("--version", required=True, help="New semantic version (e.g. 0.1.1-testnet)") |
| 65 | + p.add_argument("--title", required=True, help="Short release focus description") |
| 66 | + p.add_argument("--apply", action="store_true", help="Write changes to disk") |
| 67 | + p.add_argument("--preview", action="store_true", help="Preview without writing") |
| 68 | + args = p.parse_args() |
| 69 | + |
| 70 | + current = read_version_py() |
| 71 | + if current == args.version: |
| 72 | + print(f"Version already at {current}; no bump performed.") |
| 73 | + sys.exit(0) |
| 74 | + |
| 75 | + updated_version_py = update_version_py(args.version, apply=args.apply) |
| 76 | + changelog_entry = append_changelog(args.version, args.title, apply=args.apply) |
| 77 | + rn_path, rn_content = create_release_note(args.version, args.title, apply=args.apply) |
| 78 | + |
| 79 | + print("--- Version.py Updated ---") |
| 80 | + print(updated_version_py.splitlines()[0:8]) |
| 81 | + print("--- Changelog Entry ---") |
| 82 | + print(changelog_entry) |
| 83 | + print("--- Release Note Path ---") |
| 84 | + print(rn_path) |
| 85 | + if not args.apply and not args.preview: |
| 86 | + print("(Run with --preview or --apply)") |
| 87 | + |
| 88 | +if __name__ == "__main__": |
| 89 | + main() |
0 commit comments