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¶
A tag decides one thing: which version of the code an entry is about. Everything the changelog says about that code is a record you can maintain, so it is read live from the working tree. Fix a typo, a kind, a scope or the modules of an entry from two releases ago, and the next docs build shows the fix under the same release.
Deleting a fragment does not erase the entry. It renders from the last version the history has, so a correction made before the deletion stays.
To take an entry out, set
retracted: truein its frontmatter. Deleting is not a retraction, because a revert deletes the fragment too, and a reverted feature still shipped in the release before the revert.An
id:is the fragment’s identity. Add, change or remove one after the release and the entry stays where it is under its new key, and a fragment with anid:keeps its entry through a rename.moneta new,createandcommitwrite one. A fragment without one is known by its filename, so renaming it is a new entry. Git sees the same thing: one file deleted and another added.
--content as-published renders everything as it shipped.
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.