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:
@@ -1,7 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to Moon Household Budget. Newest first.
|
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
|
## 0.1.0 — 2026-06-06 — First tracked release
|
||||||
|
|
||||||
|
|||||||
49
CLAUDE.md
Normal file
49
CLAUDE.md
Normal 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
26
changelog.json
Normal 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
94
release.py
Normal 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))
|
||||||
38
version.py
38
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
|
Cut releases with: python release.py <version> "<what changed>" ...
|
||||||
(newest first), then keep CHANGELOG.md in sync. The version chip in the footer
|
That prepends an entry to changelog.json and regenerates CHANGELOG.md. The
|
||||||
links to /changelog, which renders these entries.
|
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_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "changelog.json")
|
||||||
CHANGELOG = [
|
|
||||||
{
|
try:
|
||||||
"version": "0.1.0",
|
with open(_CHANGELOG_PATH) as f:
|
||||||
"date": "2026-06-06",
|
CHANGELOG = json.load(f)
|
||||||
"title": "First tracked release",
|
except (OSError, json.JSONDecodeError):
|
||||||
"changes": [
|
CHANGELOG = []
|
||||||
"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.",
|
__version__ = CHANGELOG[0]["version"] if CHANGELOG else "0.0.0"
|
||||||
"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.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user