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

View File

@@ -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

49
CLAUDE.md Normal file
View File

@@ -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 <version> "<plain-language change>" "<another change>" [--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`.

26
changelog.json Normal file
View File

@@ -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."
]
}
]

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))

View File

@@ -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 <version> "<what changed>" ...
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"