Changelogs and Versioning

Every piece of software changes over time. New features are added, bugs are fixed, APIs evolve, and occasionally things break. Version numbers and changelogs are the mechanisms by which you communicate these changes to your users. When done well, they build trust, reduce upgrade anxiety, and make your project predictable. When done poorly — or not at all — users are left guessing whether an update will break their code, and they hesitate to upgrade at all.

Semantic Versioning (SemVer)

Semantic Versioning, commonly called SemVer, is the most widely adopted versioning scheme in software. It uses a three-part version number: MAJOR.MINOR.PATCH. Each part has a specific meaning that communicates the nature of the changes in a release.

  • MAJOR (e.g., 1.x.x to 2.0.0): Incremented when you make incompatible API changes. A major version bump tells consumers: "Something has changed that may break your existing code. Read the migration guide before upgrading."
  • MINOR (e.g., 1.1.x to 1.2.0): Incremented when you add functionality in a backward-compatible manner. A minor version bump tells consumers: "There are new features available, but your existing code will continue to work without changes."
  • PATCH (e.g., 1.2.1 to 1.2.2): Incremented when you make backward-compatible bug fixes. A patch bump tells consumers: "We fixed bugs. Your existing code will work the same way, but better."

When to bump each version number

The decision of which version number to bump is a communication act, not a technical one. It is a promise to your users about the nature of the changes. Here are guidelines:

  • Bump MAJOR when you remove or rename a public API method, change the type or structure of a return value, change required parameters, drop support for a platform or runtime version, or make any change that requires users to modify their code when upgrading.
  • Bump MINOR when you add a new endpoint, method, or feature; add new optional parameters to existing methods; deprecate (but do not yet remove) existing functionality; or add new configuration options with sensible defaults.
  • Bump PATCH when you fix a bug without changing the public API, improve performance without behavior changes, update documentation, or fix security vulnerabilities that do not change the API surface.

Pre-release versions use suffixes like 1.0.0-alpha.1, 1.0.0-beta.2, or 1.0.0-rc.1. These signal that the release is not yet stable and the API may still change. Build metadata can be appended with a plus sign: 1.0.0+build.123.

Keep a Changelog Format

A changelog is a curated, chronologically ordered list of notable changes for each version of a project. The Keep a Changelog project defines a widely adopted format that organizes changes into clear categories:

  • Added for new features
  • Changed for changes in existing functionality
  • Deprecated for features that will be removed in upcoming releases
  • Removed for features that have been removed
  • Fixed for bug fixes
  • Security for vulnerability fixes

Here is an example of what a changelog entry looks like in this format:

## [1.3.0] - 2025-03-15

### Added
- New /api/v2/reports endpoint for bulk report generation
- Support for PDF export in mega reports
- Dark mode toggle in application settings

### Changed
- Improved scan performance by 40% through parallel processing
- Updated minimum Node.js version to 18 LTS

### Fixed
- Fixed race condition in concurrent scan requests
- Corrected timezone handling in scheduled scans

### Security
- Updated dependency lodash to 4.17.21 to address CVE-2021-23337

The key principles of a good changelog:

  • Write for humans, not machines: Changelogs are for people. Use clear, descriptive language that explains the impact of each change, not just the technical details.
  • Put the newest version first: Readers typically want to know what changed recently, so reverse chronological order is the standard.
  • Include dates: Use the ISO 8601 format (YYYY-MM-DD) for clarity across international audiences.
  • Link to version diffs: At the bottom of your changelog, include links that show the full diff between versions on your hosting platform.
  • Maintain an "Unreleased" section: Keep a running section at the top for changes that have been merged but not yet released. This makes the release process smoother.

Conventional Commits

Conventional Commits is a specification for writing standardized commit messages. By following a simple, consistent format, commit messages become machine-readable, enabling automated changelog generation, version bumping, and release management.

The format is:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Common commit types include:

  • feat: A new feature. Maps to a MINOR version bump in SemVer.
  • fix: A bug fix. Maps to a PATCH version bump in SemVer.
  • chore: Maintenance tasks that do not affect the published code (updating build scripts, CI configuration, etc.).
  • docs: Documentation-only changes.
  • style: Code style changes (formatting, semicolons) that do not affect functionality.
  • refactor: Code changes that neither fix a bug nor add a feature.
  • test: Adding or correcting tests.
  • perf: Performance improvements.
  • ci: Changes to CI/CD configuration files and scripts.

To signal a breaking change (MAJOR version bump), add BREAKING CHANGE: in the commit footer or append ! after the type:

feat!: redesign the scan results API response format

BREAKING CHANGE: The scan results endpoint now returns a nested
object structure instead of a flat array. See migration guide
for details.

Conventional Commits work best when enforced with tooling. commitlint can be configured as a Git hook (via husky) to reject commit messages that do not follow the convention. This ensures consistency across all contributors.

Automating Release Notes from Commit Messages

One of the greatest benefits of Conventional Commits is that your entire release process can be automated. When commit messages follow a predictable format, tools can parse them to determine what changed, calculate the next version number, generate a changelog, and create a release — all without human intervention.

release-please

release-please is a Google-maintained tool that automates releases for projects using Conventional Commits. When you push to your main branch, release-please opens (or updates) a pull request that bumps the version number and updates the changelog. When you merge that PR, it creates a GitHub Release with the generated notes and a Git tag. It supports monorepos and multiple programming languages.

semantic-release

semantic-release is a fully automated version management and package publishing tool. It analyzes commit messages since the last release, determines the next version number based on Conventional Commits, generates release notes, publishes the package (to npm, PyPI, etc.), and creates a Git tag and GitHub Release. The entire process runs in CI with no human intervention.

Both tools follow the same fundamental principle: your commit history is the source of truth for what changed, and the automation derives everything else from it. This eliminates the manual effort of maintaining changelogs, deciding version numbers, and writing release notes.

Git Tags for Releases

Git tags mark specific points in your repository's history as important. For releases, annotated tags are the standard mechanism for associating a version number with a specific commit.

# Create an annotated tag for a release
git tag -a v1.3.0 -m "Release version 1.3.0"

# Push the tag to the remote repository
git push origin v1.3.0

Best practices for Git tags:

  • Use the v prefix: While not strictly required, prefixing tags with v (e.g., v1.3.0) is the dominant convention and makes version tags easy to distinguish from other tags.
  • Use annotated tags, not lightweight tags: Annotated tags (git tag -a) store the tagger name, date, and a message. Lightweight tags are just pointers to a commit and lack metadata.
  • Never move or delete published tags: Tags that have been pushed to a remote are a contract with your users. Moving or deleting them breaks reproducibility and trust.
  • Tag on the release commit: The tag should point to the exact commit that represents the release, which is typically the commit that bumps the version number in your manifest file.

Many platforms (GitHub, GitLab) automatically create release pages from tags, making tags the bridge between your Git history and user-facing release documentation.

Why Changelogs Build Trust with Users

Changelogs are more than a technical artifact — they are a communication channel between you and your users. A well-maintained changelog builds trust in several ways:

  • Transparency: Changelogs show that you are honest about what changed, including breaking changes and security fixes. Users appreciate knowing what they are getting into before they upgrade.
  • Predictability: When combined with semantic versioning, changelogs give users confidence about the scope and risk of an upgrade. A patch release with only bug fixes is a safe, routine update. A major release with breaking changes requires planning.
  • Professionalism: Maintaining a changelog signals that you take your project seriously and respect your users' time. It shows that releases are deliberate, not accidental.
  • Reduced support burden: When users can read the changelog to understand why something changed, they do not need to open a support ticket or file an issue asking "what happened?"
  • Accountability: A changelog creates a permanent record of every change. If a regression is introduced, the changelog helps identify when and why it happened.

Projects without changelogs force users to read commit logs, diff source code, or guess at what changed. This creates friction, uncertainty, and ultimately distrust. Investing a few minutes per release in maintaining a clear changelog pays dividends in user confidence and adoption.

Resources