Automate versioning + changelog on every deploy (v0.2.0)

- changelog.json is now the single source of truth; version.py reads it.
- release.py: one command to bump version + prepend a changelog entry + regen
  CHANGELOG.md. Dogfooded to cut 0.2.0.
- repo CLAUDE.md documents the MANDATORY 'cut a release on every deploy' rule
  + deploy/secrets conventions, so it applies to anyone who pulls the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 17:00:47 -05:00
parent e381b615d6
commit 99b4dd2368
5 changed files with 190 additions and 24 deletions

94
release.py Normal file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Cut a release for Moon Household Budget.
Usage:
python release.py <version> "<change 1>" "<change 2>" ... \\
[--title "Short title"] [--date YYYY-MM-DD]
Prepends a new entry to changelog.json (the single source of truth that drives
__version__, the footer version chip, and the /changelog page) and regenerates
CHANGELOG.md to match.
Versioning: semver-ish — bump the patch (0.1.0 -> 0.1.1) for fixes, the minor
(0.1.0 -> 0.2.0) for new features. Run this for EVERY deploy (see CLAUDE.md).
"""
import json
import os
import sys
from datetime import date
HERE = os.path.dirname(os.path.abspath(__file__))
CHANGELOG_JSON = os.path.join(HERE, "changelog.json")
CHANGELOG_MD = os.path.join(HERE, "CHANGELOG.md")
def load():
try:
with open(CHANGELOG_JSON) as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
return []
def render_md(data):
lines = [
"# Changelog",
"",
"All notable changes to Moon Household Budget. Newest first.",
"Generated from changelog.json by release.py — do not edit by hand.",
"",
]
for rel in data:
head = f"## {rel['version']}{rel.get('date', '')}"
if rel.get("title"):
head += f"{rel['title']}"
lines += [head, ""]
lines += [f"- {c}" for c in rel.get("changes", [])]
lines.append("")
return "\n".join(lines)
def main(argv):
if len(argv) < 2:
print(__doc__)
return 1
version = argv[1]
title, rel_date, changes = "", date.today().isoformat(), []
i = 2
while i < len(argv):
if argv[i] == "--title" and i + 1 < len(argv):
title = argv[i + 1]; i += 2
elif argv[i] == "--date" and i + 1 < len(argv):
rel_date = argv[i + 1]; i += 2
else:
changes.append(argv[i]); i += 1
if not changes:
print("error: provide at least one change bullet")
return 1
data = load()
if data and data[0].get("version") == version:
print(f"error: version {version} is already the latest entry")
return 1
entry = {"version": version, "date": rel_date}
if title:
entry["title"] = title
entry["changes"] = changes
data.insert(0, entry)
with open(CHANGELOG_JSON, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
with open(CHANGELOG_MD, "w") as f:
f.write(render_md(data))
print(f"released v{version} ({rel_date}) with {len(changes)} change(s)")
print(" -> changelog.json + CHANGELOG.md updated. Now commit + deploy.")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))