Skip to content

What changelog does not do

The quickest way to understand a small module is to know where it stops. Everything here is absent on purpose, and none of it is planned; if you need one of these, you need something around changelog, not a setting inside it.

There is no changelog command

changelog is a library. It ships no binary, has no CLI, reads no configuration file and no environment variable. go install gives you nothing to run.

Generation takes a repository path and options as Go arguments; that is the entire input surface. A tool that wants a changelog generate command builds one and calls GenerateFromRepo from it — which is exactly how it is used inside the projects it was extracted from.

It does not write, merge or update a file

GenerateFromRepo returns a string. It does not write CHANGELOG.md, does not look for an existing one, and does not prepend a new release to a file you already have.

The reason is that generation is not incremental. The whole document is rendered from git history every time, so there is no "new part" to prepend — regenerating produces the current truth, and the previous file is superseded rather than appended to. Writing that string somewhere is a one-line decision your program is better placed to make than this module is.

The output format is fixed

No templates, no configuration, no theming. Heading levels, bullet characters, category names and their order are all constants — see generated output format for exactly what they are.

The output format is fixed because it is also an input format: what GenerateFromRepo emits, Parse reads back. A configurable emitter would need a configurable parser to match, and the two would drift apart the first time someone changed one of them. Keeping one shape means a changelog this module wrote is a changelog it can still read.

If you need different markdown, parse the output into the model and render it yourself — Changelog, Release and Entry are the stable interface, not the text.

There is no commit metadata in the output

No hashes, no dates, no authors, no links to commits, merge requests or issues, no compare URLs, no contributor list.

This is about the OUTPUT, and it still holds. CommitsFromRepo returns a Commit carrying the hash and the raw commit type, but nothing it returns reaches the rendered markdown, Entry gains no field, and Parse is unchanged. The rendered changelog carries exactly what it did before, for exactly the reason below: it renders only what it can read back. See reading commits and spec 0001.

That is a real limitation and worth being clear about, because tools like releaser-pleaser and git-cliff do emit those. The trade is deliberate: this module renders only what it can also read back, and a link is forge-specific — it needs a project URL, a host, and a path convention that differ between GitLab, GitHub and everything else. Adding them would mean either a template system or a forge dependency, and the module's whole premise is a dependency graph of four libraries.

If you want links, generate the model here and render the markdown in the tool that already knows your forge.

It does not filter by scope or author

You cannot ask for "only scope cli" or "only commits by someone". Every commit reachable from the walk is classified and every surviving entry is emitted.

WithSinceTag and WithMaxReleases bound the output by release; WithPaths and WithPathFilter bound it by the files a commit changes, which is what a repository holding several components needs. Those are the axes. Filtering by anything else means parsing the generated changelog — or walking the history yourself — and selecting entries from the model.

It does not read the commit body

Only the subject line becomes an entry. The body is read for a single thing: whether a BREAKING CHANGE: footer is present. The footer's text, any Refs: or Reviewed-by: trailers, and the explanation of what actually broke are all discarded.

This is the one absence that costs a reader the most, and it is worth knowing before you write a careful breaking-change footer expecting to see it in the changelog. You will see the subject line under Breaking Changes and nothing else.

It does not validate anything

changelog reads history; it never rejects it. A commit that is not a Conventional Commit is dropped, not reported. There is no lint mode, no "which commits were ignored" list, and no count of what was skipped — a repository with no conforming commits and no tags produces an empty string and a nil error, which looks exactly like a repository with no commits at all.

If you need commits validated, that belongs in a commit hook or a CI check, where the author can still fix the message.

Its notion of a version is semver, and only semver

Tags that are not valid semver are invisible to it — see how commits are attributed to releases. The test is semver.IsValid on the tag with a v prefix added, so a calendar version, a sequential release number or a date tag is only usable if it happens to be valid semver: 2026.7, 42 and 20260703 are accepted, 2026.07 (leading zero) and nightly are not. There is no option to supply a version pattern.

What Parse will not do for you

  • It does not sort. Releases comes back in the document's order, reversed — see accepted changelog formats.
  • It does not understand the keep-a-changelog section vocabulary. Added, Changed, Removed, Deprecated, Fixed and Security all become Other.
  • It does not read markdown structurally, so bullets inside fenced code blocks become entries.
  • It does not fetch anything. ParseFromArchive takes an io.Reader; downloading the release archive is the caller's job, and the module makes no network calls of any kind.