This document illustrates a strategy used for versioning software projects that is portable across different languages and technologies.
Having a universal way of versioning software development projects is a good thing to help us keep track of what's going on.
From Wikipedia (https://en.wikipedia.org/wiki/Software_versioning): "Software versioning is the process of assigning either unique version names or unique version numbers to unique states of computer software. [...] Modern computer software is often tracked using two different software versioning schemes: an internal version number that may be incremented many times in a single day, such as a revision control number, and a released version that typically changes far less often, such as semantic versioning or a project code name."
Semantic Versioning
The most common type of versioning, that is also a de-facto standard in the Linux world, is the Semantic Versioning (http://semver.org), that is used amongst others by RPM and DEB packages. The Semantic Versioning basically prescribes 3 integer numbers separated by a dot (MAJOR.MINOR.PATCH) that are incremented in the following way:
- MAJOR version when you make incompatible API changes;
- MINOR version when you add functionality in a backwards-compatible manner;
- PATCH version when you make backwards-compatible bug fixes.
Single Source of Truth (SSOT)
The version number should be manually written only once in a Single Source of Truth (SSOT) (https://en.wikipedia.org/wiki/Single_source_of_truth).
In our case the version SSOT is a text file named "VERSION" in the root folder of the project.
Once the semantic software version is stored in the VERSION file, it can be parsed and used in multiple contexts (reusability):
- directly searched and accessed by script languages;
- injected during compilation time in the source code, so we can
return its value by invoking the API (e.g. a "version"
argument in a command-line application or a "status" entry
point in a RESTful interface);
- In GO language this can be achieved using the ldflags
argument, for example:
go build -ldflags '-X main.ServiceVersion=$(cat VERSION) -X main.ServiceRelease=$(cat RELEASE)'
- In CMake we can extract the version parts in this way:
file(STRINGS VERSION VERSION_FILE_CONTENT) string(REPLACE "." ";" VERSION_FILE_PARTS ${VERSION_FILE_CONTENT}) list(GET VERSION_FILE_PARTS 0 VERSION_MAJOR) list(GET VERSION_FILE_PARTS 1 VERSION_MINOR) list(GET VERSION_FILE_PARTS 2 VERSION_PATCH) set(PROJECT_VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") message(STATUS "PROJECT_VERSION='${PROJECT_VERSION}'")
file(STRINGS RELEASE PROJECT_RELEASE) message(STATUS "PROJECT_RELEASE='${PROJECT_RELEASE}'")
- In GO language this can be achieved using the ldflags
argument, for example:
- used to name or tag the software packages (e.g. RPM, DEB, TAR.GZ, Docker Images, …)
- used to tag the software repository so we can easily track back the released versions;
- checked by a git hook before pushing (see the rndpwd example), so we can be sure that the version is correctly updated at any software change.
All the above operations are usually automated via build scripts (e.g. Makefile) and/or CI/CD tasks.
Release Number
During the automatic build and release process performed by a CI/CD system (e.g. Jenkins, GoCD, TravisCI, ...), another unique ever-increasing build (or release) number is automatically generated. This number is usually appended, separated with a dash, to the semantic version to form the full distribution "number". For example, the number "1.2.3-45" indicates the 45th build of the 1.2.3 version of the software. This allows us to track back and read the test, build and deployment logs belonging to a specific build or software package.
Using the same strategy used for the version number, the release number can be stored in a file named "RELEASE" in the root of the project. This file is initialized with the value 1 and updated only on the build agent by the CI/CD system. The presence of the RELEASE file, even if not updated, allows us to develop and test the build and packaging procedures locally.
Project Structure
The resulting project structure should be:
. ├── ... : other project files ├── VERSION : version file (1.2.3) └── RELEASE : release file (1)
Having the VERSION and RELEASE files in the root of each project allows us to reuse the same versioning procedures across multiple projects written in different programming languages and ensure traceability of the source code from the built binaries and packages.
See also: Software Structure