docs: add CHANGELOG.rst and render it in the Sphinx docs - #521
Conversation
The repository had no changelog at all -- release history lived only in git tags and GitHub Releases. - `CHANGELOG.rst` at the repository root, 632 lines, covering 0.13.0 through a new `1.5.0 -- unreleased` section - `docs/source/changelog.rst`, a stub that only does `.. include:: ../../CHANGELOG.rst`, so no content is duplicated - `changelog` added to the second toctree in `docs/source/index.rst` - `pyproject.toml` `[project.urls].changelog` repointed from the Releases page to the rendered page Root rather than `docs/` because `MANIFEST.in` carries both `global-include *.rst` and `prune docs`: a changelog kept only under `docs/` would be missing from every sdist, which is where packagers read it. A copy in both places would drift. The stub deliberately has no title of its own -- measured, one carrying its own title renders two `<h1>` elements. `:start-after:` was rejected for silently dropping the file's intro line. Nitpicky build warning count is unchanged at 1166 with an identical warning set, and the file parses clean under plain docutils, so the sdist copy survives a PyPI-style render.
Replaces the single root CHANGELOG.rst with the layout the release chain actually needs: a Markdown CHANGELOG.md at the root holding only the newest version's entry, and the full history under docs/ as .rst wired into Sphinx. - docs/source/changelog/<version>.rst: 37 per-version entries, split out of the 632-line root file. Concatenating the preamble and all 37 in toctree order reproduces CHANGELOG.rst byte-for-byte (32,671 bytes, 632 lines). - docs/source/changelog.rst: title, preamble and a 37-entry toctree, already reachable from docs/source/index.rst:33. - CHANGELOG.md: generated, not maintained. 40 lines of GFM holding the 1.5.0 entry, for the Create Release body and the sdist surface, both of which read Markdown rather than reStructuredText. - util/changelog_md.py: the generator, plus a --check mode that exits non-zero when the committed CHANGELOG.md has drifted from its source entry. "Newest" comes from the toctree's first line rather than a version sort, since PEP 440 ordering and lexical ordering disagree on 1.5.0b3 against 1.5.0 against 1.10.0. A residual-markup guard of 13 patterns refuses to emit when any unconverted RST survives, so nothing malformed can reach a published release. - MANIFEST.in: include CHANGELOG.md explicitly, since global-include *.rst does not match it, and ship the per-version entries. CHANGELOG.md is Markdown by deliberate exception to the .rst convention: its consumers are GitHub's release renderer and PyPI, neither of which reads RST. 34 tests, 37 subtests. Sphinx warning set unchanged against main. sdist ships CHANGELOG.md and all 37 entries, and twine check passes.
|
✅ GOOD TO MERGE — combined branch reviewed at the seam; the 37-entry split independently reconstructs |
The trailer explained which `docs/source/changelog/<version>.rst` the entry came from. CHANGELOG.md is read on a GitHub release page and on PyPI, where the reader has no repository in front of them, so that half was noise to its only two audiences while the link is useful to both. Regenerated accordingly; `--check` confirms the committed file is in step.
|
✅ GOOD TO MERGE — re-reviewed the delta since the last verdict ( |
Re-review of the delta (previous verdict covered
|
Replaces the single root
CHANGELOG.rstwith the layout the release chain actually needs.The layout
CHANGELOG.mddocs/source/changelog/<version>.rstdocs/source/changelog.rstutil/changelog_md.py--checkdrift gateCHANGELOG.rstCHANGELOG.mdis Markdown by deliberate exception to this repo's.rstconvention. Its consumers are GitHub's release renderer and PyPI; neither reads reStructuredText.Nothing is maintained twice
The point of generating rather than promoting: there is no hand-off step to forget, and the root file cannot drift from the history, because one is derived from the other. That mirrors what
pcapkit/const/**already gets frompcapkit/vendor/**.At release time the work is: date the newest entry (or add a new one), put its line at the top of the toctree, and run the generator.
--checkexits non-zero with a diff ifCHANGELOG.mdis not what its source entry produces."Newest" comes from the toctree's first line, not a version sort. Passing the version by hand would reinstate exactly the hand-off this design removes, and lexical ordering disagrees with PEP 440 on
1.5.0b3against1.5.0against1.10.0. The toctree has to be newest-first anyway for the rendered history to read correctly, so it is already the authority.Verification
Content identity. Concatenating the preamble with all 37 entries in toctree order reproduces
CHANGELOG.rst@b117b976bbyte-for-byte: 32,671 bytes, 632 lines. Independently reproduced three times — by the split, by a cross-review, and by me.The generator. Byte-identical output at 17,849 bytes.
--checkexits 0 in step and 1 with a unified diff after a one-character edit. 34 tests, 37 subtests.Sphinx. Warning set unchanged against
main, compared as sets after normalising line numbers rather than by raw count. Every entry page renders oneh1and noh2;changelog.htmlis a 37-link index. All 37 parse clean under plain docutils at the most sensitive report level.sdist.
twine checkPASSED.CHANGELOG.mdand all 37 entries ship.docs/source/changelog.rstdeliberately does not — a bare toctree would not render standalone, which is the class of problem that blocked1.5.0b1from PyPI. The controls prove theMANIFEST.inline is load-bearing:CODE_OF_CONDUCT.mdandCONTRIBUTING.mdare both absent from the tarball, becauseglobal-include *.rstdoes not match*.md.The residual-markup guard
A generator that copies what it cannot convert would put literal RST into a published release body. So
render()refuses to emit when anything unconverted survives — 13 patterns, each proven to be the sole guard for its construct withuncovered: none.Eight were added after review found them slipping through silently: inline and anonymous hyperlink references, substitution references, field lists, line blocks, grid tables, simple tables, and a setext underline absorbed into prose by the unwrap rule. That last one exposed a real bug: the heading rule fires only when the underline matches the title's length exactly, but RST only requires it to be no shorter, so a longer one fell straight through.
The docstring enumerates the guarded set item by item and names the two cases it deliberately does not catch, rather than claiming "unconverted RST" generally.
Known limitations
[^n], which cmark-gfm — what PyPI uses — prints literally, since footnotes are not in its extension set. Not live: the 1.5.0 entry has no footnotes. But the first release entry that carries one would ship raw marker text to PyPI. Flagged for a decision rather than silently accepted.--checkare not yet wired into theMakefileor CI, so the drift gate is a procedure rather than a mechanism.make changelog/changelog-checktargets and a one-line CI step are drafted but belong with the workflow change.Two things genuinely lost
There is no longer a single scrollable page of the whole history — it is an index plus 37 pages. And GitHub's repo view shows
CHANGELOG.md(latest only) rather than the full file, though all 37 entries render individually there. The generated file's trailer links to the rendered history, so a reader of the root file always has a path to the rest.