Writing a good changelog, like writing any text, is about knowing your audience and their needs.

In the last post I’ve talked about the different needs of developers a changelog addresses. In this article, I would like to talk more about the specific elements that make a changelog work.

A changelog should have an entry for each version. That sounds self explanatory, but unfortunately bears repeating. Ideally, it would show the release date for each release. This makes it easier to scan the changelog and get a feel for the development speed of the project and makes it super obvious when a project is not really maintained anymore. While we’re at dates, please use a non ambiguous date format, such as the standard ISO format 2018-09-11, which is clearly the 11th September of 2018.

The changelog should be sorted so that the latest versions come first. There’s no way a user is interested in the first changes to your project, so this reverse order makes it easy to quickly scan for stuff that is actually new and thus relevant.

Here’s a good example from the flask framework:

Version 1.0.2

Released on May 2nd 2018

  • Fix more backwards compatibility issues with merging slashes between a blueprint prefix and route. (#2748)
  • Fix error with flask routes command when there are no routes. (#2751)

Version 1.0.1

Released on April 29th 2018

  • Fix registering partials (with no name) as view functions. (#2730)
  • Don’t treat lists returned from view functions the same as tuples. Only tuples are interpreted as response data. (#2736)
  • […]


Each version entry should contain a list of changes, grouped by the type of change. These different types could be, for example:

  • Added
  • Changed
  • Deprecated
  • Removed
  • Fixed
  • Security

This makes it much easier to scan the changelog for the type of entries you’re currently interested in. Again, it makes sense to clearly mark breaking changes with a bit of emphasis.

Here’s a good example, using a slightly different grouping, from the devise changelog:

4.2.1 - 2017-03-15

  • removals
    • Devise::Mailer#scope_name and Devise::Mailer#resource are now protected methods instead of public.
  • bug fixes
    • Attempt to reset password without the password field in the request now results in a :blank validation error. Before this change, Devise would accept the reset password request and log the user in, without validating/changing the password. (by @victor-am)
    • Confirmation links now expire based on UTC time, working properly when using different timezones. (by @jjuliano)
  • enhancements
    • Notify the original email when it is changed with a new Devise.send_email_changed_notification setting. When using reconfirmable, the notification will be sent right away instead of when the unconfirmed email is confirmed. (original change by @ethirajsrinivasan)

Most importantly, though: keep it consistent. If you have a full team working on the changelog, have clear guidelines in place on how you want your changelog to look. A good policy to follow, if you don’t want to come up with your own, is the one outlined on keepachangelog.com.

Keep the signals high, noise low

It’s also important to note what should not go into a changelog. For example, you should probably not include notes on internal refactorings, unless they resulted in a notable performance change, because similar to your API documentation, a changelog should only document changes to the public API. Also, you should not document changes in your team structure or things like “moved the website to GitHub pages” which are, again, mostly irrelevant for your users.

One thing we’re seeing a lot is that a changelog lists updated dependencies. I’m a bit torn on this, as this actually can effect you, if, for example, this means upgrading a dependency that you’re also using in a different context, but on the other hand, specifically for npm modules, this can be quite noisy and since npm can isolate subdependencies, is probably not relevant.

In general, if a change has no impact on your users, err on the side of brevity and leave it out.

Also, while it is nice to include references to actual pull requests or commits so that you can take a quick peek at the actual code changes, please do not simply dump a git hash into the text. Instead you should link to the diff view of the PR or the commit range for the release, if you must with the PR number or a shortened hash as the link text. A commit hash is just gibberish that impacts readability and you should always optimize your changelog for quick scanning by a human reader.

Credit where credit is due

One last thing: More and more we’re seeing references to the committers in the changelog, therefore trying to give credit to the individual contributors. While this also can be quite noisy, especially for versions with lots of individual commits in a project with many contributors, I actually think it’s a very good idea. It may not be ultimately relevant for you while upgrading, for example, but it does show that a project values its contributors and also reminds us that open source does not write itself.

Now that we know how our changelog should look, the next post will be about the actual process of writing changelogs. We’ll take a look at a couple of tools to automate parts of the process and we’ll take a look at workflows that make sure the changelog is not forgotten. Follow us on Twitter if you want to get notified.