loading...
Cover image for My Favorite Way to Do a Changelog

My Favorite Way to Do a Changelog

iffy profile image Matt Haggard ・2 min read

If you publish code for others to use, it's very beneficial to also publish a Changelog. I won't rehash what's already written in Keep a Changelog. But I will share my conflict-free method for maintaining such a log.

The Method

Let's assume your changelog is in CHANGELOG.md. Instead of accumulating unreleased changes at the top of that file in an "Unreleased" section, put each entry in a separate file:

  1. Create a ./changes/ directory
  2. For any change worth noting, commit a new file in ./changes named CHANGETYPE-decription-of-change.md. For example: fix-issue-2345.md or breaking-no-longer-support-cobol.md
  3. When it's time to make a new release, gather up the files and add them to the top of CHANGELOG.md, grouped and labeled by change type.
  4. Then delete the files.

By using this method you don't have to fix conflicts in CHANGELOG.md, either when merging or when backing out a change.

Other Options

Instead of using markdown snippets, you could use a more structured format if you want more structure (e.g. TOML, YAML, XML, JSON, etc...). I haven't needed this yet.

Example Script

It's fairly simple to automate this in your project's language. Here's an example of doing this with Nim.

import os
import strutils
import strformat
import times
import tables

if paramCount() < 1:
  let prog = getAppFilename().splitFile.name
  echo &"usage: {prog} NEWVERSION"
  quit(1)

let today = now().format("yyyy-MM-dd")
let newversion = &"[{paramStr(1)}] - {today}"
let changedir = "changes"

# Option: You could make the script look in CHANGELOG.md and
# guess the next version based on the presence of fixes, breaking
# changes, or new features.

var changes = initOrderedTable[string, seq[string]]()
changes["breaking"] = @[]
changes["fix"] = @[]
changes["new"] = @[]
changes["doc"] = @[]
changes["misc"] = @[]

for file in changedir.walkDir:
  let changetype = file.path.splitFile.name.split("-")[0]
  if changes.hasKey(changetype):
    var prefix = ""
    case changetype
    of "breaking": prefix = "**BREAKING CHANGE:** "
    of "fix": prefix = "**FIX:** "
    of "new": prefix = "**NEW:** "
    changes[changetype].add("- " & prefix & file.path.readFile.strip)
  else:
    changes["misc"].add("- " & file.path.readFile.strip)

# Option: You could make this handle multi-line entries

echo &"## {newversion}"
echo ""
for entries in changes.values:
  for line in entries:
    echo line

# Option: You could make this update CHANGELOG.md in place

What the sample change snippets look like:

$ grep "" changes/*
changes/breaking-foo.md:I broke a thing
changes/doc-heyo.md:Fixed documentation
changes/fix-foo.md:Another thing
changes/fix-something.md:I did something

Running it:

$ nim c -r changelog.nim 1.2.0
## [1.2.0] - 2020-09-15

- **BREAKING CHANGE:** I broke a thing
- **FIX:** Another thing
- **FIX:** I did something
- Fixed documentation

Discussion

pic
Editor guide