From 99b4dd23685c16e08d3f3c173a215db0861fe554 Mon Sep 17 00:00:00 2001 From: tonym Date: Sat, 6 Jun 2026 17:00:47 -0500 Subject: [PATCH] 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 --- CHANGELOG.md | 7 +++- CLAUDE.md | 49 ++++++++++++++++++++++++++ changelog.json | 26 ++++++++++++++ release.py | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++ version.py | 38 ++++++++------------ 5 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 CLAUDE.md create mode 100644 changelog.json create mode 100644 release.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf55c5..74dc441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog All notable changes to Moon Household Budget. Newest first. -This file mirrors the structured changelog in `version.py` (shown at `/changelog`). +Generated from changelog.json by release.py — do not edit by hand. + +## 0.2.0 — 2026-06-06 — Automatic version + changelog on every update + +- Every app update now automatically bumps the version and adds a 'What's New' entry, so you always see what changed. +- Added a one-command release tool and wrote the process into the project so any future change stays documented. ## 0.1.0 — 2026-06-06 — First tracked release diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..424a9b9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,49 @@ +# Moon Household Budget + +Personal household-budgeting web app. Bonna (Tony's wife) is the primary user. +Self-hosted; also serves as a **testbed** for a future low-cost SaaS (planned +price point ~$2.00 — do **not** build billing yet, just keep the option open). + +## Stack +- **Flask + gunicorn** (1 worker + threads — state lives in files on the volume, + so multiple processes would race). Jinja templates all extend `base.html`. +- **Storage:** small data = JSON files via `storage.py` (atomic writes, + crash-safe loads). **Transactions = SQLite** (`txns_db.py`, `transactions.db`) + with the same list-of-dicts API as the old JSON. +- **Settings/secrets:** `/settings` page → `settings.py` → `settings.json` on the + volume (never in git). `reminders.py` reads Gmail creds from there. +- **Suggestions (💡):** `suggestions/` blueprint files feedback as Gitea issues + (needs `GITEA_URL/GITEA_TOKEN/GITEA_REPO` in `.env`). +- **Version chip + changelog:** `version.py` + `/changelog`, sourced from + `changelog.json`. + +## ⚠️ Release process — REQUIRED on every deploy +Every deploy MUST cut a release so users see what changed. Before deploying: + +```sh +python release.py "" "" [--title "..."] +``` + +- Bump the **patch** (0.1.0 → 0.1.1) for fixes, the **minor** (0.1.0 → 0.2.0) + for new features. +- Write changes in plain language Bonna will understand (not code-speak). +- This updates `changelog.json` (drives `__version__`, the footer chip, and + `/changelog`) and regenerates `CHANGELOG.md`. Commit both with your change. +- **Do not deploy without a new changelog entry.** + +## Deploy (Docker on Unraid NAS — http://192.168.0.5:8052) +```sh +# from the repo root, after committing (incl. the release entry): +rsync -a --exclude='.git/' --exclude='data/' --exclude='.env' --exclude='__pycache__/' \ + ./ root@192.168.0.5:/mnt/user/appdata/moon-household-budget/ +ssh root@192.168.0.5 'cd /mnt/user/appdata/moon-household-budget && docker compose up -d --build' +``` +- **Never** rsync `data/` or `.env` — that's live data and secrets on the volume. +- Templates are baked into the image, so template changes need `--build`. +- Backups: the `backup` sidecar tars `/data` to `/mnt/user/Backup/moon-household-budget/` + daily; pushes to Backblaze B2 only once a real bucket/key is in `.env`. + +## Conventions +- Secrets never go in source — use `.env` (container) or `settings.json` (volume). +- Keep other small JSON files as JSON; only `transactions` warranted SQLite. +- Repo lives on the home Gitea: `bonna61/Moon-Household-Budget`. diff --git a/changelog.json b/changelog.json new file mode 100644 index 0000000..b62fdb7 --- /dev/null +++ b/changelog.json @@ -0,0 +1,26 @@ +[ + { + "version": "0.2.0", + "date": "2026-06-06", + "title": "Automatic version + changelog on every update", + "changes": [ + "Every app update now automatically bumps the version and adds a 'What's New' entry, so you always see what changed.", + "Added a one-command release tool and wrote the process into the project so any future change stays documented." + ] + }, + { + "version": "0.1.0", + "date": "2026-06-06", + "title": "First tracked release", + "changes": [ + "Crash-safe data storage \u2014 saves are now atomic and a corrupted file can no longer take the app down.", + "Transactions moved to a SQLite database: faster, and safe when edited from two tabs at once. Existing transactions were migrated automatically.", + "New Settings page (\u2699\ufe0f) to store your Gmail login and reminder preferences securely \u2014 no passwords in the code anymore.", + "Suggestions & feedback (\ud83d\udca1) is now reachable from every page; what you send becomes a tracked issue.", + "Bill email reminders now use the Gmail login from Settings.", + "Automatic daily backups of all your data (local for now; Backblaze cloud once a bucket is set up).", + "Fixed the Monthly view crashing on the budget total.", + "Added this version number and changelog." + ] + } +] diff --git a/release.py b/release.py new file mode 100644 index 0000000..9d9044b --- /dev/null +++ b/release.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Cut a release for Moon Household Budget. + +Usage: + python release.py "" "" ... \\ + [--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)) diff --git a/version.py b/version.py index 02a04d1..f14902a 100644 --- a/version.py +++ b/version.py @@ -1,26 +1,18 @@ -"""App version + changelog. +"""App version + changelog, sourced from changelog.json (single source of truth). -To cut a release: bump __version__ and prepend a new entry to CHANGELOG -(newest first), then keep CHANGELOG.md in sync. The version chip in the footer -links to /changelog, which renders these entries. +Cut releases with: python release.py "" ... +That prepends an entry to changelog.json and regenerates CHANGELOG.md. The +footer version chip and the /changelog page read from here automatically. """ -__version__ = "0.1.0" +import json +import os -# Newest first. Each release lists what's new in plain language. -CHANGELOG = [ - { - "version": "0.1.0", - "date": "2026-06-06", - "title": "First tracked release", - "changes": [ - "Crash-safe data storage — saves are now atomic and a corrupted file can no longer take the app down.", - "Transactions moved to a SQLite database: faster, and safe when edited from two tabs at once. Existing transactions were migrated automatically.", - "New Settings page (⚙️) to store your Gmail login and reminder preferences securely — no passwords in the code anymore.", - "Suggestions & feedback (💡) is now reachable from every page; what you send becomes a tracked issue.", - "Bill email reminders now use the Gmail login from Settings.", - "Automatic daily backups of all your data (local for now; Backblaze cloud once a bucket is set up).", - "Fixed the Monthly view crashing on the budget total.", - "Added this version number and changelog.", - ], - }, -] +_CHANGELOG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "changelog.json") + +try: + with open(_CHANGELOG_PATH) as f: + CHANGELOG = json.load(f) +except (OSError, json.JSONDecodeError): + CHANGELOG = [] + +__version__ = CHANGELOG[0]["version"] if CHANGELOG else "0.0.0"