Docket

AGENTIC WORKFLOWS

Writing changelogs from commits

A commit log and a changelog are written for different readers, so turning one into the other is a translation job, not an export.

6 MIN READ Last updated 23 August 2026

A commit log is not a changelog. Commits are written for whoever maintains the code; a changelog is written for whoever uses it. Turning one into the other is translation: pull the raw commits, then rewrite them into sentences a customer would recognise. A coding agent does the second half well. Never trust it with the first.

This page is one part of a larger guide. For the whole subject in one place, see changelogs and release notes.

This page is one part of a larger guide. For the whole subject in one place, see running support with a coding agent.

Why isn't a commit log already a changelog?

Because it was never written for that reader. A commit message like "fix null check in export worker" is exactly right for the next engineer, who has the file open. A changelog reader has the opposite needs: they will never see that file, and they only want to know whether the thing that broke for them is fixed.

Pasting commit subjects straight into a changelog serves neither reader. It is too vague for the engineer and too technical for the customer. How to write release notes covers the writing craft in full. This page is the step before that: getting the raw material out of the repository in a shape worth rewriting.

How do you pull the raw material with git log?

Start narrow, one line per commit:

git log --pretty=format:"%h %s" v1.2.0..v1.3.0

%h is the short hash, %s is the subject, and v1.2.0..v1.3.0 is the range between two tags, everything reachable from the second that is not reachable from the first. Add the author and date when a subject alone does not explain enough:

git log --date=short \
  --pretty=format:"%h  %ad  %an: %s" \
  v1.2.0..v1.3.0

That is wrapped to fit this page; the real command is one line. Merge commits are noise here, their subject is usually just a branch name, so drop them:

git log --no-merges --pretty=format:"%h %s" v1.2.0..v1.3.0

How do you select the range between two tags?

List tags newest first and take the top two:

git tag --sort=-creatordate | head -n 2

If you do not tag releases, git log accepts a commit or date range instead: git log --pretty=format:"%h %s" HEAD~40..HEAD for the last forty commits, or git log --since="2 weeks ago". A date range is a weaker anchor, since it does not line up with what actually shipped, so treat it as a fallback.

What difference do Conventional Commits make?

A large one. Conventional Commits prefixes every message with a type, feat:, fix:, chore:, docs:, before the description, so the category a changelog needs is already sitting in the commit rather than something an agent has to infer. Filtering becomes a grep on the prefix:

git log --grep="^feat" --pretty=format:"%h %s" v1.2.0..v1.3.0

Without that convention, the work moves later: an agent reads each subject, sometimes the body, and guesses a category from context. The guess is usually right, but it is a guess where a prefix gave a fact. The cheapest fix for an unlabelled history is not retrofitting old commits, it is adopting the prefixes from the next commit onward, the same partial-adoption approach Keep a Changelog recommends for its own six categories.

What is a coding agent actually good at here?

Three things, all downstream of having the raw commit list in front of it. Grouping: sorting forty commits into added, changed and fixed takes a person real time and an agent a few seconds. Deduplicating: a single feature often lands across several commits, an implementation, a follow-up fix, a typo correction, and a changelog should describe that as one line, not four. Rewriting: turning "refactored the search index to use a trigram matcher" into "search now finds results even with a typo in your query" is exactly the translation an agent is fast at, once it knows who the reader is.

What does it get wrong?

Judging what matters. An agent reading a commit range has no idea which change generated forty support emails and which one nobody will ever notice. A dependency bump and the feature everyone asked for are both, to a language model, well-formed sentences describing a change, and left unsupervised it will give them equal weight. It also cannot know a fix closes a specific bug report unless told, which is the strongest argument for linking commits to the request that caused them.

The fix is not a smarter prompt. It is a human step: read the grouped draft, cut what nobody will notice, and reorder so the thing customers asked for is not sitting under a bullet about a linting change.

What does the before and after actually look like?

Six realistic commits from a release range:

a1b2c3d fix: null check in csv export worker
e4f5a6b chore: bump lodash 4.17.20 to 4.17.21
9c8d7e6 feat: add CSV export to roadmap board
f1e2d3c wip
4b5c6d7 fix(auth): handle expired session token on refresh
7a8b9c0 refactor: extract debounce helper from save handler

An agent given that list, and told who reads a changelog, should produce something close to this:

Added

Fixed

Three commits disappeared, correctly. The dependency bump changed nothing a reader could see. wip is a placeholder, not a description of anything. The debounce refactor is real work with no visible effect on its own. What survived is the two changes a customer could actually notice, described in the words they would use to notice them.

Your own support centre, in your own repository

One payment, no subscription, unlimited products.

How do you turn this into a repeatable habit?

The steps are the same every time, which is what makes them worth repeating. Pull the range since the last tag, split it by Conventional Commit prefix where you have one, hand the list to an agent with instructions to group, deduplicate, and write the effect rather than the mechanism, then read the draft yourself before it goes anywhere. That read is not optional; it is the one step that knows what your customers care about, and nothing upstream of it does.

Once the entries are written, where they land matters too: the Keep a Changelog format gives the grouped output a standard structure, so a returning reader knows where to look without relearning your layout every release.

Frequently asked questions

Do I need Conventional Commits to do this well?

No, but it removes a step. With the prefixes, category is a fact already in the message and a grep sorts most of the work. Without them, an agent has to read each subject and infer a category, which mostly works but is slower and occasionally wrong in a way a labelled prefix cannot be.

How do I choose which commits to include?

Between the current tag and the previous one, if you tag releases, using git log v1.2.0..v1.3.0. If you do not tag, a commit range like HEAD~40..HEAD or a time window with --since is a reasonable fallback, though it will not line up as cleanly with what actually shipped together.

Should I let the agent publish the changelog automatically, with no review?

No. Grouping and rewriting are exactly what an agent is fast at, but deciding what customers care about is a judgement call it cannot make from a commit message alone. Keep a human read between the draft and the published page, every time.

What do I do with merge commits and reverts?

Drop merge commits with --no-merges, since their subject is usually just a branch name. A commit that reverts an earlier one should cancel it out entirely rather than appearing as two changelog lines, one adding a behaviour and one removing it a day later.

Isn't this what GitHub's automatic release notes already do?

GitHub's automatic release notes are closer than a raw commit list, since they group by pull request and label rather than by commit, but it still speaks in engineering terms: pull request titles, not customer sentences, with no judgement about which of ten merged pull requests is the one customers were waiting for. It is a reasonable starting list to feed an agent, not a finished changelog on its own.