Semantic Versioning

The Echo project adheres to Semantic Versioning. Semantic Versioning is a software versioning scheme. In this scheme, the version numbers and the way the version numbers change convey meaning about:

  • the maturity of the underlying code

  • the nature of change from one version to the next

To give a brief, non-exhaustive overview:

  • A normal version number must take the form X.Y.Z where X, Y, and Z are non-negative integers.

  • X is the major version, Y is the minor version, and Z is the patch version.

  • Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.

  • Version 1.0.0 defines the public API. The way in which the version number is incremented after this release is dependent on this public API and how it changes.

  • Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced.

  • Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced to the public API.

  • Major version X (X.y.z | X > 0) MUST be incremented if any backwards incompatible changes are introduced to the public API.

Please refer to the Semantic Versioning documentation to get the full picture.

1. Automated Semantic Versioning

There are tools that support Semantic Versioning. Given a well-define syntax for commit messages. And given that syntax conveys whether the commit contains breaking or non-breaking bug fixes or functionality. When all commit messages in a repository adhere to this syntax, then a tool can

  1. derive the correct version number according to Semantic Versioning from the commit messages

  2. automatically apply that version during the CI build

Example syntax for commit messages are:

There is a wide range of tooling that automate semantic versioning based on commit messages that follow these conventions.

Be warned though, not all of these tools were meant to be used within a monorepo and therefore should not be used within a monorepo. A lot of these tools assume that only a single version must be maintained for the entire repository. These tools consider all commits on the git repository to calculate the next version. However, in a monorepo you most probably have multiple versions to maintain.

Then, there are the tools that are monorepo aware. But even those might not do want you want.

For example, in Echo’s monorepo, the REST API component and the GraphQL API component should each have their own version. The REST API consists of:

  • echo-rest-api-app (app)

  • echo-rest-api-controllers (lib)

  • echo-rest-api-model (lib)

A breaking change in any of these modules is considered a breaking change to the REST API and must bump up the major version of the REST API.

The GraphQL API consists of:

  • echo-graphql-api-app (app)

  • echo-graphql-api-model (lib)

  • echo-graphql-api-resolvers (lib)

A breaking change in any of these modules is considered a breaking change to the GraphQL API and must bump up the major version of the GraphQL API.

However, a breaking change to the REST API must not bump up the major version of the GraphQL API and vice versa.

automated semantic versioning challenge
Figure 1. Separate Versions for REST API and GraphQL API

To achieve this, the following is required:

  1. The tool that calculates the semantic versions must:

    1. understand which group of modules share the same version

    2. calculate the version for each group of modules separately

    3. take only those commits into account that are relevant to these group of modules

  2. Once that tool calculated the semantic versions, these groups of modules must be released with those versions.

The Echo project is not the only project with these requirements. The need to version groups of modules was raised by others. For example, the Nx plugin @jscutlery/semver started a discussion on this topic in February 2021.

Unfortunately, I couldn’t find a tool that meets the requirements mentioned in point 1 above. If I had such a tool, then could resolve point 2 with the technique described in Maven CI Friendly Versions. After applying that technique, all Maven modules have the same version that can be set by a single property: revision. The default value of the revision property is some snapshot version. Once an application like echo-rest-api-app must be released with a newly calculated semantic version, the following commands will build that application with that new semantic version:

./mvnw clean install                 \  (1)
  --projects :echo-rest-api-app      \  (2)
  --batch-mode                       \  (3)
  --also-make                        \  (4)
  -Drevision=<new-semantic-version>  && (5)
./mvnw package spring-boot:repackage \  (6)
  --projects :echo-rest-api-app      \
  --batch-mode                       \
  --DskipTests                       \  (7)
  -Drevision=<new-semantic-version>     (8)
1 First, we have to clean and install the application and all its dependencies.
2 Specifies the project/module/application we want to build.
3 Ensures that Maven doesn’t log every downloaded kB.
4 Tells Maven to clean and install also all the dependencies of the project.
5 Sets the revision property to the newly calculated semantic version. Thanks to Maven CI Friendly Versions, Maven now builds the project and its dependencies with this version.
6 Now, we have to repackage the application. We have to do that in a second Maven call. We cannot add the spring-boot:repackage goal to the first Maven call. If we did, Maven would try to execute the spring-boot:repackage goal also on the dependencies. Since the dependencies are not Spring Boot applications, the command would fail. Note that the spring-boot:repackage goal cannot be executed separately. It must be executed along with at least the package goal, or else it fails.
7 The first Maven call already executed the tests. Therefore, we can skip the tests in the second Maven call.
8 Sets the revision property to the newly calculated semantic version.

The Maven commands above build an application of choice (including its dependencies) with a version of choice. These commands could be part of a release target in the applications' project.json. That way, one could release affected applications in the Nx workspace with a command as follows:

npx nx affected --target=release

However, as long as I don’t find a semantic versioning tool that allows me to configure groups of modules that share the same version, I cannot automate semantic versioning for the Echo project. At least not the way I want.