Commit & changelog conventions
CHANGELOG.md is generated from git history at release time, never
hand-edited on a branch. Editing it on every branch is what produced the endless
rebase and merge conflicts; generating it from commit metadata removes them. Two
things drive the generator, and both are enforced on every merge request:
- the module prefix — which selects the changelog marker, and
- an optional
Changelog:trailer — which supplies the bullet text.
Commit subjects
Every commit subject is module: summary with a bare module prefix — the
name of a repo subsystem, never a change-type (there is no feat:/fix:) and
never the parenthesized module(scope): form. The prefix is not decoration: the
generator routes each module to a changelog marker, so an unlisted or mistyped
prefix (e.g. serer: for server:) would silently become a user-facing
changelog line. make commit-subjects.check rejects anything outside the
allowed set.
The full list — each module with a one-line description — is the single source
of truth in
scripts/commit-prefixes.txt,
which is exactly what the check script reads. The module decides the changelog
marker:
- The product modules —
server,templates,pdfrender,services,config— render as a plain bullet (user-facing). tester(the React UI) renders as [tester].- The internal modules —
web,examples,ci,build,devenv,e2e— render as INTERNAL. releaseseals a version and is skipped by the generator.
Merge commits are exempt (their subjects are auto-generated).
Recording a changelog entry
A commit contributes a bullet only if it carries a Changelog: git trailer.
Put the terse bullet in the trailer; keep rationale and detail in the body,
which never reaches the changelog:
ci: content-address the base images
Free-form rationale and detail live here, in the body. None of it appears in
the changelog.
Changelog: Content-address CI base images so they rebuild only when inputs change
The commit's prefix picks the marker, so the trailer above renders as:
- **INTERNAL** Content-address CI base images so they rebuild only when inputs change
- Keep the trailer terse — it is the changelog line, read at a glance.
- Purely mechanical commits (typo fixes, formatting) need no trailer.
- For a change that is genuinely note-worthy but should produce no bullet,
add a
Changelog: skiptrailer. That is the only opt-out — a conscious, in-history acknowledgment, not a hidden label.make changelog.checkfails a merge request in which no commit carries a realChangelog:trailer and none carriesChangelog: skip.
One feature split across several commits usually wants one bullet: put the trailer on the commit that best represents the change, and leave the supporting commits (tests, CI wiring, docs) trailer-less.
Running the checks locally
Both gates default to the master..HEAD range, exactly as CI runs them on a
merge request:
make commit-subjects.check # every subject uses an allowed bare prefix
make changelog.check # the change records at least one Changelog: trailer
make changelog.preview # preview the bullets the next release will contain
Point the range at a different base with MR_BASE=<ref> (both scripts also
honor GitLab's CI_MERGE_REQUEST_DIFF_BASE_SHA when it is set).
Cutting a release
Releasing stays manual; only the changelog step is automated:
make changelog.preview # review what will be included
make changelog VERSION=vX.Y.Z # prepend the generated release section
Review the diff, merge to master, then tag vX.Y.Z on master and push — the tag
drives the release pipeline, as before.
Where the pieces live
| File | Role |
|---|---|
scripts/commit-prefixes.txt | Source of truth for the allowed module prefixes |
scripts/commit-subject-check.sh | Subject-prefix gate (make commit-subjects.check) |
scripts/changelog-check.sh | Changelog: trailer gate (make changelog.check) |
cliff.toml | git-cliff config: marker rules + the CHANGELOG.md template |
AGENTS.md | The short version of these rules for coding agents |