1 Sep 2022 · Software Engineering

    Semantic Versioning with CI/CD

    8 min read
    Contents

    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 Metafont does the same with e. Its current version sits at 2.71828182.

    We all did this: add versions of a file by appending copy, copy of copy, copy final, copy REALLY FINAL...
    All of us are guilty of this.

    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:

    1. Major : incrementing this number (1.0.0 -> 2.0.0) indicates users should expect significant breaking changes.
    2. 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.
    3. 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:

    <header>
    <BLANK LINE>
    <body>
    <BLANK LINE>
    <footer>

    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 TypeResult
    fix, perfBump version to the next patch level (1.0.0 -> 1.0.1) and release
    featBump version to the next minor level (1.0.0 -> 1.1.0) and release
    docs, 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:

    Commit messageResult
    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 version
    Examples of commit messages

    How to get started with semantic-release

    While semantic-release is a Node-based application, it supports any language, not just JavaScript or TypeScript. You’ll need to have Node and NPM installed to use it, though.

    To add semantic releases to your project, follow these steps:

    1. Install and run the semantic-release wizard with npx semantic-release-cli setup. When asked which CI platform to use, select Other and copy the environment variables shown. You’ll get a token for GitHub and, optionally, one for NPM.
    export GH_TOKEN=ghp_Lw83uUpu4paBlLKQuRijD3NMTDusAL07J89l
    export NPM_TOKEN=npm_xWtDqAasy9yBTPPuA6QppCJx7JIu5w1009KY8
    1. Go to your project’s Git repository. If the project runs on Node.js, add the semantic-release package with:
    npm --save-dev semantic-release
    1. Make some code changes and create a commit following the commit guidelines discussed before. For example:
    feat: initial commit
    1. Run 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)
    ​
    ### Features
    ​
      * initial commit
    1. 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.

    A CI pipeline consisting of a build job, followed by unit, integration, and e2e tests.
    The starting CI pipeline builds and tests a JavaScript Node.js project.

    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.

    Disable the tags checkbox in the project workflow build settings.
    Do not trigger CI builds on Git tags.

    Authentication secrets

    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.

    1. Go to your organization menu and click on Settings:
    The settings menu is located under the organization button on the top-right of the main screen.
    1. Click on Secrets > New Secret
    2. The name of the secret should be semantic-release-credentials. Add your GitHub and/or NPM tokens as shown:
    Secret called

    Continuous delivery pipeline with semantic versioning

    Let’s add a continuous delivery pipeline to automatically release new versions of the project.

    1. Open your project on Semaphore and edit the workflow.
    2. Click on +Add promotion to create a new pipeline. Enable automatic promotions if you want to release new versions automatically.
    Creating a new promotion for the CD pipeline
    1. Select the new block and add the following commands. sem-semantic-release is a thin wrapper around the tool that handles the installation and exports release information into the pipeline.
    checkout
    sem-semantic-release
    1. Open the secrets section and enable the secret created earlier.
    A new pipeline contains a job that runs the checkout command followed by sem-semantic-release. The secret created earlier is enabled.
    1. Click on Run the workflow > Start to test your pipeline.

    The final CI/CD workflow. Contains a CI pipeline and the new CD pipeline with the semantic release job.
    The continuous delivery pipeline has released a new version.

    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.1
    docker build . -t my-awesome-app:$(sem-context get ReleaseVersion)

    You can check what information is available in the sem-semantic-release docs.

    Conclusion

    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!

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I picked up most of my skills during the years I worked at IBM. Was a DBA, developer, and cloud engineer for a time. After that, I went into freelancing, where I found the passion for writing. Now, I'm a full-time writer at Semaphore.