Semantic Versioning with CI/CD
Software is constantly changing — the moment it is released it is already becoming obsolete. Users need a constant stream of patches and demand new features. At the same time, updates introducing a breaking change are unwelcome, especially when this happens without warning. Semantic versioning is one of the most popular solutions for this.
Since forever version numbers and code names have been commonly used to track releases. Many projects use incrementing sequences (MS-DOS 6.2, 6.21, 6.22) while others use part of the release date (Ubuntu 18.04, 20.04, 22.04). Some use a more whimsical approach: TeX, for example, uses a numbering scheme that asymptotically approaches π (the current version is 3.141592653), while Metafontdoes the same with e. Its current version sits at 2.71828182.
One of the drawbacks with all of these versioning approaches is that they don’t tell us anything about compatibility between releases. The way to find out is to dig through changelogs.
What is semantic versioning?
Semantic versioning is a versioning scheme that aims to communicate the level of compatibility between releases at a glance. It uses a three-part numbering system: major.minor.patch (e.g. 1.2.3) which may or may not be suffixed with special identifiers such as -alpha or -rc1.
Each part has a different meaning:
- Major : incrementing this number (1.0.0 -> 2.0.0) indicates users should expect significant breaking changes.
- Minor: the minor number (1.0.0 -> 1.1.0) is incremented when non-breaking features and changes have been released. Minor releases should be backwards-compatible.
- Patch: a patch-level change (1.0.0. -> 1.0.1) is a non-breaking upgrade that introduces low-risk changes like fixing bugs or patching security issues.
A developer can quickly assess the risk of upgrading by comparing version numbers. Major releases are risky and should be planned carefully. Minor and patch-level changes are much less likely to introduce incompatibilities and are safer to install.
Automating versions with semantic-release
How do we determine the version number in a semantic versioning scheme? It’s a tricky question since a typical release includes dozens of commits. Some contain bug fixes, while others may introduce breaking changes. In such a scenario, the only way to determine the appropriate version is by reviewing each commit individually and assessing the impact. If this sounds like a lot of repetitive and error-prone work that would be best completed using an automation tool, you’d be right.
Semantic release is a versioning tool that can compute semantic version numbers by reading commit messages. It can also generate release notes and publish packages to GitHub and NPM.
As you might imagine, for this to work, commit messages should follow a predefined pattern:
The header is the only mandatory part of the message and should be uniformly formatted:
<type>(<scope>): <short summary>
The most important component of the header is the type, which helps semantic-release assess the importance of the changes introduced in the commit. The default behavior follows Angular’s message format:
Header TypeResultfix, perfBump version to the next patch level (1.0.0 -> 1.0.1) and releasefeatBump version to the next minor level (1.0.0 -> 1.1.0) and releasedocs, build, ci, refactor, testNo version bump. No release.Types of commit messages
Regardless of the header type, if the body of the commit message contains the string BREAKING CHANGE or DEPRECATED, semantic release performs a major version increase.
To clarify, let’s look at a few examples and their outcomes:
fix(pencil): stop graphite breaking when too much pressure appliedRelease a patch
feat(pencil): add 'graphiteWidth' optionRelease a minor version
perf(pencil): remove graphiteWidth option
BREAKING CHANGE: The graphiteWidth option has been removed. The default graphite width of 10mm is always used for performance reasons.Release a major versionExamples of commit messages
How to get started with semantic-release
To add semantic releases to your project, follow these steps:
- Install and run the semantic-release wizard with
npx semantic-release-cli setup. When asked which CI platform to use, select
Otherand copy the environment variables shown. You’ll get a token for GitHub and, optionally, one for NPM.
- Go to your project’s Git repository. If the project runs on Node.js, add the
npm --save-dev semantic-release
- Make some code changes and create a commit following the commit guidelines discussed before. For example:
feat: initial commit
npx semantic-release. In non-CI environments, the tool runs in dry-run mode. The log shows what version would be assigned in the next release (in the example below, v1.0.0).
ℹ Running semantic-release version 19.0.5
⚠ This run was not triggered in a known CI environment, running in dry-run mode.
✔ Allowed to push to the Git repository
✔ Completed step "verifyConditions" of plugin "@semantic-release/npm"
✔ Completed step "verifyConditions" of plugin "@semantic-release/Github"
ℹ No git tag version found on branch master
ℹ No previous release found, retrieving all commits
ℹ There is no previous release, the next release version is 1.0.0
ℹ Start step "generateNotes" of plugin "@semantic-release/release-notes-generator"
✔ Completed step "generateNotes" of plugin "@semantic-release/release-notes-generator"
⚠ Skip v1.0.0 tag creation in dry-run mode
ℹ Release note for version 1.0.0:
# 1.0.0 (2022-08-23)
* initial commit
- When you’re ready to release, execute:
npx semantic-release --no-ci. This will tag the release and publish the package.
You can customize the tool’s behavior by creating a .releaserc or release.config.js file in the project’s root. This will allow you to tweak the commit message format, safelist the branches capable of triggering a release, and enable optional plugins. For more details check the configuration docs.
Semantic versioning with CI/CD
In this section, we’ll configure a CI/CD pipeline to perform semantic versioning. You will need to have installed semantic-release and already have a continuous integration pipeline.
Before we can add continuous delivery, we need to make two changes in Semaphore:
- Disable tags so releases don’t trigger CI builds.
- Add a secret containing the GitHub or NPM credentials.
Disable tags on Semaphore
Semaphore triggers CI builds on all branches and Git tags by default. The problem with this behavior is that semantic-release creates and pushes a tag on every new version. So, if we don’t disable (or safelist) tags on Semaphore, the tool’s release may trigger secondary (and useless) CI/CD runs.
Change the build settings by opening the Semaphore project settings and scrolling down to What to build?. Ensure the tags option is unchecked.
Semaphore will publish releases to GitHub and NPM on your behalf, so will need access to your authorization credentials which we’ll store using a Secret.
- Go to your organization menu and click on Settings:
- Click on Secrets > New Secret
- The name of the secret should be
semantic-release-credentials. Add your GitHub and/or NPM tokens as shown:
Continuous delivery pipeline with semantic versioning
Let’s add a continuous delivery pipeline to automatically release new versions of the project.
- Open your project on Semaphore and edit the workflow.
- Click on +Add promotion to create a new pipeline. Enable automatic promotionsif you want to release new versions automatically.
- Select the new block and add the following commands.
sem-semantic-releaseis a thin wrapper around the tool that handles the installation and exports release information into the pipeline.
- Open the secrets section and enable the secret created earlier.
- Click on Run the workflow > Start to test your pipeline.
This setup will execute semantic-release on each commit or merge into the master branch. Depending on the content of the commit messages, the tool might bump the version numbers and publish the release
Extending the delivery pipeline
Executing sem-semantic-release in the CI environment exports special information about the release. For instance, you can determine if a release occurred by executing sem-context get ReleasePublished in a later job.
We can use these details to perform more advanced workflows or continuous deployments. Let’s say we want to build a Docker image and tag it with the release number. We can use a command along these lines for that:
# e.g. builds my-awesome-app:2.0.1docker build . -t my-awesome-app:$(sem-context get ReleaseVersion)
You can check what information is available in the sem-semantic-release docs.
Maintaining consistent version numbers will help you gain the trust of users and other developers. Simple projects may only require manual versioning, but highly active codebases with several contributors won’t tolerate this. The only sensible alternative is to automate the release chores and remove the human element from the middle with a tool such as semantic-release.
Happy releasing, and thanks for reading!
Originally published at https://semaphoreci.com on September 1, 2022.