The Sphinx extension#

moneta_sphinx renders the changelog into a Sphinx site. It runs moneta render --format json at build time and turns the result into docutils nodes, so the page is rebuilt from the tags on every build rather than from a file somebody remembered to update.

# conf.py
extensions = ["myst_parser", "moneta_sphinx"]
```{moneta-changelog}
```

That is the whole setup for a docs/ directory inside the repository being documented: with no moneta_repo set, the extension walks up from the source directory to the enclosing checkout.

Directive options#

Option

:versions:

all (default), unreleased, released, a tag, or <older>..<newer>

:kinds:

render only these kinds, comma- or space-separated

:scope:

render only entries carrying one of these scopes

:heading-depth:

2 (default), 1 or 0 — see below

:no-unreleased:

leave out the Unreleased section

:repo:

read a different repository, relative to the source directory

Filtering happens after moneta has run, not as flags to it, so a site with several changelog pages — one per scope, say — is still one subprocess.

:heading-depth: is a depth rather than a level because the level the headings come out at is decided by where the directive sits in the page, which docutils resolves and the extension cannot. What it controls is how many of the two generated levels are real headings:

2 : A section per version and a section per kind. Everything is linkable and everything is in the sidebar.

1 : A section per version; the kinds become rubrics. This is what a project with fifty releases wants, rather than three hundred sidebar entries.

0 : No headings at all, for embedding the changelog under a page’s own. The version anchors still exist, so cross-references keep working.

Cross-references#

Every rendered version is an object in the moneta domain:

The fix shipped in {moneta:version}`v1.2.0`.

The tag prefix is optional — {moneta:version}`1.2.0` resolves too — since which form is the version’s name is a property of the project’s tag-prefix and prose should not have to know it. With nitpicky = True, a reference to a release the site no longer renders fails the build.

Configuration#

Value

Default

moneta_binary

"moneta"

the binary to run

moneta_repo

None

the repository to read; relative paths resolve against the source directory

moneta_json

None

read this file instead of running moneta

moneta_args

[]

extra arguments for moneta render

moneta_body_parser

"myst"

"myst" or "rst"

moneta_versions

"all"

the default for :versions:

Entry bodies are Markdown, so the default parses them with MyST; each body is parsed into a document of its own rather than through the host page’s state machine, which on a Markdown page would leave anything generated as literal text. A project whose fragments are reStructuredText sets moneta_body_parser = "rst".

moneta_json is the escape hatch for a build that cannot run git — a hermetic package build, or a shallow CI checkout, where moneta would correctly refuse. Render the document in a job that has the full history and carry the file into the one that does not.

Incremental builds#

Rebuilding the changelog page on every build is wasteful; not rebuilding it when it has changed publishes a stale one. Two mechanisms, because neither is enough alone:

  1. Every fragment file and .moneta.yaml is a dependency of the page, so editing an entry rereads it the way editing any source file does.

  2. Creating a tag changes which release an entry belongs to without touching any file, which no dependency can catch. So the extension also hashes the document on env-get-outdated and compares it with what the page last rendered.

Watching .git/HEAD and .git/packed-refs as dependencies was the obvious alternative, and it is wrong often enough to matter: a ref may be loose or packed, and .git is a file rather than a directory inside a worktree.

The hash deliberately leaves out repo.head, so an ordinary commit — one that adds no entry and no tag — rebuilds nothing.

Failing loudly#

A changelog that renders empty, or short by a release, looks exactly like a correct one. Nobody reading the page can tell. So each of these stops the build rather than producing a page:

  • moneta exiting non-zero, including its refusal to read a shallow clone. The child’s stderr is reported verbatim.

  • a schema_version the extension was not written for.

  • :versions: naming a release that is not there.

  • moneta_body_parser = "myst" with myst_parser not loaded.