Be Kind to Your Future Self with Conventional Commits and ADRs

Photo by Yancy Min on Unsplash

Be Kind to Your Future Self with Conventional Commits and ADRs

Over the years I have adopted a few practices in my coding life that have really paid dividends. When I introduce the ideas to new teams, I always put it under the guise of being kind to your future self and "engineering excellence", though the latter doesn't always get as much traction as the former. In this article, I will go into two practices that I've adopted that came up in a recent conversation.

Conventional Commits

When it comes to commit messages, there are many opinions on what makes a good one and why. Most folks will agree, though, that a simple one line commit is far from ideal. I've adopted the Conventional Commits standard with a required body that describes why I made a change, rather than just describing the change I made. Thank you Chris Beams for that idea. Here's the standard message format in a nutshell, pulled from the Conventional Commits summary:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

The commit contains the following structural elements, to communicate intent to the consumers of your library:

  1. fix: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
  2. feat: a commit of the type feat introduces a new feature to the codebase (this ?>correlates with MINOR in Semantic Versioning).
  3. BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
  4. types other than fix: and feat: are allowed, for example @commitlint/config-conventional (based on the the Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:, and others.
  5. footers other than BREAKING CHANGE: may be provided and follow a convention similar to git trailer format.

Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE). A scope may be provided to a commit’s type, to provide additional contextual information and is contained within parenthesis, e.g., feat(parser): add ability to parse arrays.

Benefits

This approach has many advantages when working with a team or on a complex project.

Firstly, when something breaks it is easy to not only find our who made a change but also why they made the change. That context can be invaluable, and you no longer have to break your flow to go look up an old ticket reference that's hopfully still valid. Couple that message with a tool like GitLens, which lets you quickly and easily see git commit messages in your IDE, and your flow is even cleaner.

Next up, this aligns beautifully with Semantic Versioning and allows for more automation around releases and version numbering. Fixes map to patches. Features map to minor increments. Breaking changes map to major. There is even some good automation already built to support this not only in your release flow, but also in your pre-commit stage. This post from jsilvax over on Medium goes into this in more detail.

Finally, ticketting systems change. While you may have kept your issues in GitHub Issues, your organization may want to move to Jira. I would hope the teams in charge of that would migrate your issues over, but do you want to rely on that? It is my opinion that the why of your changes should live with your code, in you git commit messages. As long as you keep git as your VCS, these messages will always remain.

Architectural Decision Records (ADRs)

But can the context of all changes be clearly conveyed in a commit message? What if the change is part of a larger decision made by the team? Enter Architectural Decision Records (ADRs). ADRs are a way to capture the Architecturally significant decisions made as part of a projects evolution. There are many formats, but the key is to capture what decision was made, why it was made, and who agreed to it. In addition, I like to capture the other options that were considered and why the final choice was made. I'm a fan of Markdown Any Decision Records for their flexibility and relative completeness.

By putting your ADRs in a subdirectory of your documentation, it becomes easy to reference as things move forward. Keeping this close to your code, instead of a separate system like Confluence, has many of the same advantages as those listed above for conventional commits, like ease of reference from comments/commits and the fact that it will always be along side your code even if external systems change.

A bonus feature of this approach is that if you use a static site generator, your ADRs can be published along with your normal documentation. I typically use MKDocs to generate a static site from the /docs/ directory which includes ADRs. For an example, you can check out this reference repo and the static site that it generates.