Skip to content

Give each published module its own changelog - #125

Merged
tony merged 4 commits into
masterfrom
docs/per-package-changelogs
Nov 1, 2026
Merged

tony merged 4 commits into
masterfrom
docs/per-package-changelogs

Conversation

@tony

@tony tony commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Why

libtmux-java publishes ten Maven modules, libtmux, libtmux-mcp, libtmux-workspace, libtmux-workspace-cli, libtmux-kotlin, libtmux-scala, libtmux-scala-cats, libtmux-scala-ox, libtmux-jackson and libtmux-junit5, but kept one CHANGELOG.md for all of them, so a reader of one module had to read the others' history to find their own, and no jar carried a changelog. The maintainer asked for one per published package and said a main changelog beside a changelog for each package is fine. Once each module has a file, nothing noticed a module losing it or a jar going out without it, so the change also extends the release check.

Modelled on libtmux-ts #146.

What

  • Splits the history into a changelog per module, every entry moved verbatim with its PR and issue links, under its original release and section heading: libtmux, libtmux-mcp, libtmux-workspace, libtmux-workspace-cli, libtmux-kotlin, libtmux-scala, libtmux-scala-cats, libtmux-scala-ox, libtmux-jackson, libtmux-junit5. The root CHANGELOG.md keeps what spans modules. Each file keeps the release headings, dates and the Unreleased heading, with an introduction naming its module.
  • Ships each module's file in its jar: libtmux.publication.gradle.kts copies the CHANGELOG.md beside a module's build file into META-INF/libtmux/<module>/, next to the artwork the convention already puts there. A Maven POM has no field for release notes, so the jar is the carrier. libtmux-bom is a platform with no jar and no changelog; its entries are in the root file.
  • Extends the staged-release check, tools/verify-staged-release.sh, to refuse a staged jar that does not hold its module's changelog, or holds one that does not open with Notable changes to and the module's name. A Scala artifact's id ends in _3; the check strips it to find the module.
  • Adds a guard to tools/ci/check.py, which the tmux-matrix plan job already runs on every pull request: every published module keeps a CHANGELOG.md that opens with its own name and holds a ## Unreleased heading outside a code fence, its README links it, and the publication convention still ships it with a from(...) line that is not commented out. The published modules are read from settings.gradle.kts, so a module added later without a changelog is caught.
  • Links each module's changelog from its README, and says in WRITING.md, CONTRIBUTING.md, RELEASING.md and AGENTS.md which file an entry goes in and that the tag commit dates every changelog.

Nothing lost

An entry is a top-level bullet with its nested content, or a blank-line separated paragraph. A script parsed the old file at e227c943 and the eleven new files separately and required every old entry to appear in exactly one new file, under its original release and section headings, with nothing added. The one difference it allows is the ../ that a relative link needs once its entry sits one directory down, which 28 entries gained.

File Entries PR and issue references
old CHANGELOG.md at e227c943 288 209
new CHANGELOG.md 69 28
new libtmux/CHANGELOG.md 129 122
new libtmux-mcp/CHANGELOG.md 55 28
new libtmux-workspace/CHANGELOG.md 4 4
new libtmux-workspace-cli/CHANGELOG.md 8 9
new libtmux-kotlin/CHANGELOG.md 12 10
new libtmux-scala/CHANGELOG.md 1 1
new libtmux-scala-cats/CHANGELOG.md 2 2
new libtmux-scala-ox/CHANGELOG.md 0 0
new libtmux-jackson/CHANGELOG.md 0 0
new libtmux-junit5/CHANGELOG.md 8 5
new total 288 209

Dropping an entry, duplicating one into a second file, changing a single word, adding an entry, or swapping two release headings makes the script fail, so it is capable of failing. The script is not in this change: it compares against one old commit, so as a gate it would go stale at the next changelog edit.

Judgement calls

  • The old file grouped entries by Added, Changed, Fixed and so on, not by module, so each entry went to the module it changes. An entry that names no module but changes a core API is a libtmux entry. The early alphas' libtmux-mcp entries (tmux_run, tmux_whoami, the safety tiers, the toolsets) rarely name the module, and I read each one to place it; the fixtures TmuxExtension, NamedServerFixture and FakeTmux are libtmux-junit5.
  • An entry that changes two modules stays in the root file, because each entry lives in exactly one: a core call together with the MCP tool that wraps it, ServerConfig.force256Colors together with tmux-workspace load -2, the sealed exception tree that also changes libtmux-jackson, the Scala and Ox LiveView entry, and FilterModel.fieldNames, which is a libtmux-jackson class that the MCP server reads. So do documentation, CI, tests, build and release tooling, libtmux-bom and the release summaries, whichever module they concern.
  • libtmux-jackson and libtmux-scala-ox have no entry of their own in the old file. Their files keep every release heading from the module's first release and say No entries for `libtmux-jackson`. under each. The line says "no entries" rather than "no change" because the old file did not record every source change in the early releases.
  • A module file starts at the release that first shipped the module: libtmux-workspace-cli at 0.0.1-alpha.16, and the three Scala modules at 0.0.1-alpha.13, the first release whose entries name them, although the first tag with their directories is alpha.15.
  • The shared status notice, "the changelog marks each break", is kept word for word in the root README and in every changelog introduction, as WRITING.md requires, so the root README does not name the module changelogs in it; the paragraph below it and the Documentation section do.
  • A relative link in a moved entry (28 of them, almost all into MIGRATION.md) would lead nowhere from the module directory, and DocumentationFactsTest fails on a link that does not resolve, so each gained ../. That is the only change to entry text, and the script checks it.

The check

The staged-release script is what the release rehearsal job in ci.yml runs on a signed staging, so the rule lives there. test_staged_release.py stages a small repository with a throwaway signing key, so every other rule of the script passes, and breaks one jar at a time: the unbroken staging passes, a Scala jar with no changelog is refused by its module name and not its artifact id, and a jar holding another module's changelog is refused. With the new block removed from the script, the two refusals fail and the unbroken staging still passes. The test skips where gpg cannot make a key.

The guard has eight tests in test_check.py, each breaking a copy of the tree: a deleted changelog, a new published module without one, another module's changelog pasted over this one, a missing Unreleased heading and one inside a code fence, a README that does not link its changelog, and a publication convention that stops shipping it. With the guard switched off, seven of the eight fail; the eighth, that the guard finds the ten jars and not the BOM, tests discovery. test_staged_release is added to the plan job's unittest line in tmux-matrix.yml, beside test_check.

Verification

Run on the final tree, through the host's heavy-job wrapper: python3 -m unittest tools.ci.test_check tools.ci.test_staged_release tools.ci.test_plan tools.ci.test_tmux_ref (83 tests, and test_staged_release fails rather than skips when CI is set and gpg cannot make a key) and python3 -m tools.ci.check; ./gradlew :docs:test --tests io.github.libtmux.docs.DocumentationFactsTest, which resolves every Markdown link in the repository, with the new changelogs in place; and processResources for libtmux, libtmux-mcp and libtmux-scala, whose built jars list META-INF/libtmux/<module>/CHANGELOG.md.

Not run locally: a full staging with a signing key (publishAllPublicationsToStagingRepository), which CI does in its release rehearsal, and the real-tmux suites.

Not for merging yet: master stays held until the docs pin wave finishes.

Closes #122.

tony added 3 commits November 1, 2026 07:35
why: libtmux-java publishes ten Maven modules that shared one
CHANGELOG.md, so a reader of libtmux-mcp or libtmux-workspace-cli had to
read the other modules' history to find their own. The maintainer
agreed a main changelog beside one per module is fine.

what:
- Move every entry verbatim into exactly one file, under its original
  release and section heading, with its PR and issue links; a relative
  link gains the ../ its new directory needs, nothing else
- Add <module>/CHANGELOG.md for the ten published modules, from the
  release that first shipped each
- Keep entries that span modules, and documentation, CI, tests, build
  tooling, the BOM and the release summaries, in the root file
- Keep the Unreleased heading and every release heading and date in each
  file; a release with nothing for a module says so
why: A module that has its own changelog should carry it to the people
who fetch the jar. The publication convention already puts each module's
artwork in the jar under META-INF/libtmux/<module>; the changelog
belongs beside it.

what:
- Copy the CHANGELOG.md beside each published module's build file into
  META-INF/libtmux/<module>/ of its jar
- Link each module README to its changelog
why: Once every module has a changelog, nothing noticed a module losing
it, or a jar going out without it: Gradle's from() skips a missing file
without a word. The staged-release check has to refuse before anything
reaches Central.

what:
- Make verify-staged-release.sh require each staged jar to hold its
  module's changelog, opening with the module's name
- Add a guard to tools.ci.check that every published module keeps a
  CHANGELOG.md with an Unreleased section, that its README links it, and
  that the publication convention ships it
- Test the guard on broken copies of the tree, and the script on a
  signed staging with a jar that lacks the file; run both in the
  tmux-matrix plan job
@tony
tony force-pushed the docs/per-package-changelogs branch from 558a5a2 to d9d98ed Compare November 1, 2026 13:36
why: A contributor should be told which file an entry goes in, and the
guides still pointed at one changelog for everything. Neither the guides
nor the root README said so once the file was split.

what:
- Say in WRITING.md which file an entry goes in, in CONTRIBUTING.md what
  is enforced, in RELEASING.md that the tag commit dates every
  changelog, and name the files in AGENTS.md
- Say in the root README that each module keeps a changelog, and in
  MIGRATION.md where the full history is
@tony
tony force-pushed the docs/per-package-changelogs branch from d9d98ed to e0bff1c Compare November 1, 2026 13:41
@tony
tony merged commit 739a77f into master Nov 1, 2026
21 of 27 checks passed
tony added a commit that referenced this pull request Nov 1, 2026
why: Cut 0.1.2 from master so the test-run sandbox guard (#123) and its
cli await fix (#130), the per-module changelogs (#125) and the Kotlin and
Scala Server capability directory (#127) ship. The install snippets and
the examples' snapshot coordinates need to name the release that ships
them.

what:
- Date the 0.1.2 section (2026-11-01) in the root changelog and in each
  module's, and keep an empty unreleased section above it; the modules
  with nothing new say so
- Move the install snippets and the examples' snapshot coordinates to
  0.1.2
- Set the development version to 0.1.2-SNAPSHOT; libtmuxApiBaseline
  stays until 0.1.2 is published
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Give each published package its own changelog

1 participant