A changelog, kept from now rather than assembled at the end

Every version bump gets an entry in the same commit. Not afterwards: the reason
a change was made is known while it is being made and gone a week later, and a
changelog assembled from commit subjects at release time is a list of things
nobody can act on.

Backfilled 0.8.2 through 0.9.8, because those shipped as a running deployment
rather than as releases and 1.0.0's notes have to be assembled from something.

The rule that earns the file its place is the last one in CLAUDE.md: a fix to
something that *looked* like it worked gets a line, always. Those are the
entries somebody stops working around a bug because of, and they are invisible
from outside -- nobody reports a control that silently does nothing, they just
quietly stop using it. Half of what is in here is that shape: a group delete
that left its grants, a share panel that only saved if you also saved the
resource, an update script that stopped after "== fetching ==".

A release is a signed annotated tag whose message is that version's entry, and
that is not decoration -- /admin/updates reads release notes out of the tag
object, so the tag message is literally what an administrator sees on the update
page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 21:36:44 +02:00
parent 4532b95559
commit 44920b62be
2 changed files with 200 additions and 0 deletions
+31
View File
@@ -1645,6 +1645,37 @@ that the right one works, because only the second half would have passed
throughout. When adding a control that writes, check the verb against the route,
and assert on the row rather than on the response.
## Releasing, and the changelog
**Every version bump gets a `CHANGELOG.md` entry, in the same commit.** Not
afterwards and not at release time: the reason a change was made is known while
it is being made and is gone a week later, and a changelog assembled from commit
subjects at the end is a list of things nobody can act on.
Four rules, and the last is the one that earns the file its place:
- **Newest first, one section per version**, headed by the version alone.
`## Unreleased` sits at the top between bumps.
- **Written for somebody using or running this**, not for somebody reading the
diff. "Deleting a group left every share naming it behind" is an entry; "call
forget_principal in delete_group" is not.
- **The version is `__version__` and nothing else.** `pyproject.toml` reads it
from there, the service worker cache is keyed on it, and the footer shows it.
- **A fix to something that *looked* like it worked gets a line, always.** Those
are the entries somebody stops working around a bug because of, and they are
invisible from the outside: nobody reports a control that silently does
nothing, they just stop using it.
A release is a **signed annotated tag** whose message is that version's entry --
`git tag -s v1.0.0 -m "$(…)"`. That is not decoration: `/admin/updates` reads
release notes out of the tag object with `git for-each-ref`, so the tag message
*is* what an administrator sees on the update page. `services/updates.py` strips
the signature block, PGP and SSH both.
The first tagged release is **1.0.0**. Everything before it shipped as a running
deployment, which is why `CHANGELOG.md` covers versions that were never released
-- 1.0.0's notes are assembled from them.
## Changing the schema
There is no Alembic, but there *is* `db/migrations.py`. It compares the declared