moneta#

A changelog built from git history.

Change entries are Markdown files under .changes. Which release each one belongs to is the first tag whose tree contains it — so a release is a tag and nothing else. No commit bumps a version, nothing is moved or deleted at release time, and the changelog follows from the history.

$ moneta new -k added -m "Support nested .changes directories" -i 42
.changes/20260923T142530Z-added-support-nested-changes-directories.md

$ moneta next
v0.2.0

$ git tag v0.2.0          # that is the entire release ceremony

The rule#

files(T)    = the fragment keys under .changes in tag T's tree
baseline(T) = the union of files(A) over every ancestor tag A of T
entries(T)  = files(T) minus baseline(T)

Three choices in there are load-bearing, and each rules out a wrong answer:

Tree presence, not “the commit that added the file” : After a rebase, a squash or a cherry-pick, which commit introduced this has several defensible answers. Was it in the release has exactly one.

A union over every ancestor, not a diff against the newest : Picking one predecessor goes wrong the first time a branch is merged. Nothing in attribution depends on semver ordering, so two tags on one commit, a mistyped tag and tags on divergent lineages cannot produce a wrong answer.

Keys, not paths : A fragment’s key is its filename stem, or an explicit id:. Moving one into a subdirectory is a delete and an add under path comparison, which would silently re-credit the entry to a later release.

Editing an entry after it has shipped#

Attribution is fixed by the tags. What an entry says is read from the working tree, so fixing a typo in an entry from two releases ago reaches the next docs build without moving it. Deleting the fragment falls back to the text its tag holds, so tidying .changes never erases history.

The one edit that does move an entry is changing its key after it has shipped: the old key stays attributed to its release while the new one matches nothing and lands in Unreleased, so the entry appears twice. moneta check fails on it and names both keys. Set id: before a release if a rename is coming.

Shallow clones#

moneta refuses to run in a shallow clone, and the refusal is the feature. A grafted history does not fail an ancestry question, it answers it wrongly, so the result would be a changelog quietly missing whole releases — published to a docs site and to a release page with nothing anywhere saying so.

In GitLab CI, set GIT_DEPTH: 0 on any job that runs moneta.

--allow-shallow degrades instead of refusing. It sets "truncated": true in the JSON and puts a warning at the top of every rendered format, so the result still says what it is.