Trying to Understand Semantic Versioning and Conventional Commits
Introduction
Recently, I’ve open-sourced several fun little projects on GitHub, such as browser extensions and some web applications. While working on a browser extension, I set up a GitHub Action pipeline for the release process. This pipeline monitors changes to the version field in manifest.json and generates a new release based on the updated version value. It was at this moment that I started pondering a question: How should I standardize my version numbers?
Up until then, I hadn’t encountered any formal guidelines on this topic. The projects I’d previously contributed to didn’t have clear commit rules or version naming conventions either. I’d only seen such standards in passing within some open-source projects or heard them mentioned casually, but I lacked a comprehensive understanding. So, I decided to dive into some research to learn how to properly standardize a project’s version numbers.
Fortunately, while asking an AI to help me standardize my commit messages, it introduced me to the concept of “conventional commits.” I soon discovered that conventional commits are closely tied to “semantic versioning.” To grasp their relationship, I first explored each concept individually and then examined how they work together.
Semantic Versioning
A software’s version number is a key identifier, and many software projects follow a standardized naming convention—one of which is “semantic versioning.”
What Is Semantic Versioning?
Let’s start with the basics of semantic versioning:
Semantic versioning is a method for describing software versions using a set of rules to reflect changes. The rules are as follows:
- A version number consists of three parts: major version, minor version, and patch version, formatted as
major.minor.patch.- Version increments follow these guidelines:
- Major version: Increments when incompatible API changes are made, resetting minor and patch versions to 0.
- Minor version: Increments when new, backward-compatible functionality is added, resetting the patch version to 0.
- Patch version: Increments for backward-compatible bug fixes.
- Pre-release tags or build metadata can be appended to the
major.minor.patchformat as an extension.
(Note: Backward compatibility means that when a user imports your package and uses its functions, updates to the package won’t require code changes, and the expected results remain unaffected.)
An Example
Let’s break this down with an example:
Suppose we have a software package at version 1.0.0, used externally, with a function example(a: string, b: number): string {}. Here’s how the three version components might change:
- Patch version: A bug exists in the
examplefunction. We fix it without introducing new APIs, ensuring the function still works as expected. Since this is just a bug fix with no new features or incompatible changes, the version becomes1.0.1. - Minor version: We add an overload to the
examplefunction, updating it toexample(a: string, b: number, c?: boolean): string {}. Existing calls still work as expected, but passing the optionalcparameter alters the output. This adds functionality without breaking compatibility, so the version becomes1.1.0. - Major version: We add a required parameter, making it
example(a: string, b: number, c: string): string {}. Existing calls now fail due to a missing parameter, introducing an incompatible API change. The version jumps to2.0.0.
Semantic versioning provides clear rules for version increments, especially when incompatible API changes occur. Its primary benefit is ensuring that users of a major version can update to new minor or patch releases without breaking their existing code.
Version Control in Early Development
A major version of zero (0.y.z) signals a special state: the software is in its initial development phase, where anything might change at any time. In this stage, the simplest approach is to start with 0.1.0 and increment the minor version with each release.
When to Release Version 1.0.0?
Once the software is deployed to production and actively used, it should reach version 1.0.0. At this point, it likely has stable APIs relied upon by users, and backward compatibility becomes a priority.
Version Precedence
How do we determine the order of priority among version numbers? When comparing versions, split them into major, minor, patch, and pre-release components (build metadata is excluded). Then apply these rules:
- For two stable releases, compare major, minor, and patch numbers sequentially until a difference is found; the higher number takes precedence (e.g.,
1.0.0 < 1.0.1 < 1.1.0 < 1.1.1 < 2.0.0). - For identical major, minor, and patch versions, a stable release always outranks a pre-release (e.g.,
1.0.0-alpha < 1.0.0). - For two pre-releases with identical major, minor, and patch numbers, compare identifiers (separated by dots) from left to right:
- Numeric identifiers are compared numerically (e.g.,
1.0.0-alpha.1 < 1.0.0-alpha.5). - Alphanumeric identifiers are compared lexicographically in ASCII order (e.g.,
1.0.0-alpha.beta < 1.0.0-alpha.rc). - Numeric identifiers have lower precedence than alphanumeric ones (e.g.,
1.0.0-0 < 1.0.0-alpha). - If initial identifiers match, the pre-release with more fields takes precedence (e.g.,
1.0.0-alpha.1 < 1.0.0-alpha.1.0).
- Numeric identifiers are compared numerically (e.g.,
A straightforward example:
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 < 1.0.1 < 1.1.0 < 1.1.1 < 2.0.0
Try applying the rules step-by-step—you’ll gain a deeper understanding of version precedence.
Conventional Commits
I mentioned earlier that conventional commits are closely tied to semantic versioning. When managing source code with tools like Git, using git commit, adhering to conventional commit standards allows us to easily bundle specific commits into a new release. The release’s version update can then be determined based on the commit types.
Many open-source projects include conventional commit guidelines in their contribution docs. For example, the Commit Guidelines section in arco-design’s CONTRIBUTING.zh-CN.md mandates that commit messages follow conventional commit standards and lists acceptable commit types.
So, let’s explore what conventional commits are.
What Are Conventional Commits?
Here’s the core concept:
Conventional commits are a structured way to describe commit messages using a set of rules. The guidelines are as follows:
- The commit message must begin with a descriptive title, starting with a commit type from this list:
- feat: A new feature (corresponds to a minor version increment in semantic versioning).
- fix: A bug fix (corresponds to a patch version increment).
- BREAKING CHANGE: An incompatible API change, noted either in the footer as
BREAKING CHANGE:or with a!after the type/scope (corresponds to a major version increment).- Other types: e.g.,
docs,chore,style,refactor,perf,test,build,ci,revert.- The structure of a commit message is:
<type>[optional scope]: <description> [optional body] [optional footer]
Here are some examples of conventional commits:
- A commit with a description, scope, and a breaking change in the footer:
feat(api): add a new feature Detailed description of the new feature. BREAKING CHANGE: This commit introduces a breaking change. - A commit using
!to flag a breaking change:feat(api)!: add a new feature Detailed description of the new feature. - A concise commit without a body:
docs: correct spelling of CHANGELOG
How Conventional Commits Relate to Semantic Versioning
| Commit Type | Semantic Version | Description |
|---|---|---|
feat |
Minor | New feature |
fix |
Patch | Bug fix |
BREAKING CHANGE |
Major | Breaking change |
| Other types | None | Other changes |
An Example
Consider a project at version 1.0.0 with these commits:
feat(api): add a new featurefix(api): fix a bugdocs: correct a typo in the documentation
To release a new version, since we’ve added a feature but no breaking changes, we increment the minor version, making it 1.1.0.
Now, imagine these commits instead:
feat(api)!: add a new feature with a breaking changefix(api): fix a bugdocs: correct a typo in the documentation
With a breaking change introduced, the next release requires a major version increment, resulting in 2.0.0.
Benefits of Conventional Commits
Adopting conventional commits offers several advantages:
- Simplifies automation of changelog generation, build triggers, and deployment workflows, streamlining version releases.
- Enables automatic determination of semantic version increments based on commit types, enhancing version management.
- Creates a structured commit history, making it easier for colleagues, the public, and stakeholders to understand changes and reducing the learning curve for contributors.
Closing Thoughts
Semantic versioning and conventional commits are interconnected concepts that help us manage software versions effectively. Semantic versioning defines rules for version increments and handles incompatible API changes, while conventional commits structure commit messages and automate version updates based on those changes.
In real-world development and project management, adhering to these standards improves software quality and maintainability. They also streamline version management and releases, boosting development and collaboration efficiency.
References
Unless otherwise stated, all articles on this blog are licensed underCC BY-NC-SA 4.0license. The author reserves all rights. Please credit the source if you wish to reprint.