Simple description
If you need to know all the details of the SemVer specification, please visit the specification website, here we will only briefly discuss the main points and some practical applications.
2.3.8-alpha3.1
^ ^ ^ ^
| | | |
| | | --> Prelaunch
| | --> Patch
| --> Minor
--> Major
- Use Patch for bug correction, 100% backward compatible
- Use Minor to add new functionality without breaking compatibility
- Use Major to introduce major changes that have the potential to break backward compatibility
- Use Pre-Release while developing a new version
Best practices in the use of Semantic Versioning
Single Source of Truth
It is of paramount importance in software development that there is a single source of truth for the entire process, and this is especially important with regard to version. There should be no question as to which version of the source code was used to build each piece of software. Choose a source of truth for the package version and make sure that version is reflected throughout the process.
Automatic sync
Once you’ve chosen the single source for the version, look for tools that allow you to automate most of the version synchronization at different stages of the software development process (SDLC). It is inevitable that the version is present in different forms and places of the process, so it is necessary that, as far as possible, the process by which the version spreads to these different stages and places be automatic to prevent a desynchronization.
For example, the version appears in a package meta file in text form, also in a repository tag and package meta description, and potentially in various other places along the way.
Intentional versioning
Don’t fall for the temptation of updating the version automatically. It is very easy for developers to create a simple utility that updates the version each time we make a change to the code or each time we compile the application. And while it is true, that saves us a few milliseconds, it goes against the best practices of the semantic version, where each change to the version is intentional and calculated in such a way that the version reflects the changes in the application.
Refuse to unintentionally rewrite the version
To avoid confusion, it is preferable that a version cannot be used twice, it is better if there is some way to prevent it by default. Systems like the npm package manager reject a second package with the same version, but allow you to delete the current version and rewrite it in case it is absolutely necessary to use a version that was used by mistake. Choose tools that allow you at some point in the pipeline to detect and reject two different packages with the same version.
SemVer in specific tools
NPM
Versioning your package or application
NPM is the NodeJS package manager and has native support for SemVer, that is, you don’t need to install a separate tool for all Semantic Versioning management in your package, although there are tools for advanced use if necessary. I think it will be enough to show an example to document how easy it is to do semantic versioning in NPM.
# start creating a new package
# (you'll have answer several questions
# like who will support the package, descripción, etc.)
npm init
# now you'll have a file called package.json
# inside which you'll the a field called version
# now bump the version
npm version patch
# verify in package.json and you'll notice
# that version is now 1.0.1
# if you are working on a version not ready for release
# you can increment the pre-release version
npm version prerelease
# version is now 1.0.2-0
# let's say you are still working on that pre-release
npm version prerelease
# version is now 1.0.2-1
# whenever you are ready to release that version
npm version patch
# version is now 1.0.2
# note that you were already working
# on 1.0.2 but it has now dropped the prerelease portion
# now you can publish this version
# rmember to always use the SemVer concepts
# meaning
# patch to fix problems
npm version minor
# now version is 1.1.0, which means that you
# have added new functionality
npm version major
# now version is 2.0.0, which means tha
# that you have changed something that could break
# the functionality of existing installations
Versioning your dependencies
As you saw, it is very easy to use SemVer when the tools you use support it natively. But, it is also necessary to use the concepts of SemVer when your application or package depends on other packages; NPM also handles this natively. For example,
# Let's say that in your package you need to parse a swagger file
# YOU MUST NOT write your own parser :)
# There is a parser called swagger-parser,
# you want to install it and you don't care about the version
npm install swagger-parser
+ swagger-parser@10.0.3
# this will install the latest version 10.0.3
# but, the library has changed since last time
# you used it and prefer to use the previous version
# If you know the exact version, you can install it like this
npm install swagger-parser@2.0.0
+ swagger-parser@2.0.0
# if you prefer the minor version 2.0,
# but you don't know what the last variation
# of that version is, you can use the tilde ~
npm install swagger-parser@~2.0.0
+ swagger-parser@2.0.1
# if you are adventurous but you don't want to
# that a new version break compatibility
# with your package, then you can
# ask for the latest minor version
# using the caret or hat ^
npm install swagger-parser@^2.0.0
+ swagger-parser@2.5.0
Another aspect of a mature package manager is creating a lock file. That is, while it is true that the developer wants a certain level of flexibility in the version of the package they receive, the application operator will want a high level of stability in the application that they keep in production. To achieve this balance, package managers like NPM create a file parallel to package.json in which they store the specific version of each of the dependencies that were installed. This is key so that, when building the version that will run in production, it contains the exact same versions that the developer used during development and that were used during unit testing and integration. Otherwise, operators could not be confident that what they are putting in production is identical to what was originally intended.
NPM has an additional advantage and that is that when you use the npm manager to version your package or application, it automatically adds a git tag to your repository, we will see it in action in the next section.
Git
Git tags are not strictly compatible with SemVer, that is, one can assign any word as a tag (although there are very strict rules regarding which characters are valid). So while almost any word is valid as a git tag, most software development teams use conventions to equate tag with version. The most used convention is to prefix the semantic version with the letter v; thus semantic version 2.3.8 will be labeled in git as v2.3.8. Again, there are no hard and fast rules, but it’s important that the whole team accepts a standard nomenclature by convention.
All modern software development systems support semantic versioning in package handling and it is important to keep git tag synchronization in parallel with the package itself. But, you must remember that git tags are not reusable (even though they can be deleted and recreated, this is not recommended), and therefore you must tag when the version is ready to be promoted.
My recommendation is that the source of truth is the version in the package and that git be synchronized manually or automatically. Use strict SemVer in versioning the package and the current convention is to add the ‘v’ to the git tag.
# as soon as you are going to work on something new, create a branch
git checkout main
git pull
git checkout -b 25-my-new-functionality
# it is also recommended to create the temporary package semver at the same time
# current version is 3.1.8
npm version prerelease
# the output will be something like v3.1.9-0
# now you can work on fixing or implementing
#encode ...
#lint
# unit test
# during implementation it is recommended
# to add files and commit multiple times daily
# it is also recommended that each time the code is pushed
# to the remote repository, update the prerelease version
# npm version prerelease
# 3.1.9-1, 3.1.9-2, etc.
npm version prerelease
git add --all
git commit -m 'I added something'
# the first time, you must indicate which remote branch to send to
git push -u origin 25-my-new-functionality
# once the code has been implemented, validated and tested
# then the appropriate version is created (patch, minor or major)
npm version minor
# now the version will be 3.2.0, note that when creating the version it removes the prerelease segment
# it is also important to note that, in the case of npm
# and other modern package managers,
# if you use the version tools,
# tags will automatically be created in git
git add --all
git commit -m 'Fixes # 25 - My new functionality'
# The --follow-tags argument instructs git to send everything, including tags to the remote repo
git push --follow-tags
# if your company does not use a process to do peer code review and the merging of branches
# then the following lines are called locally
git checkout main
git pull
git merge 25-my-new-functionality
git push
# regardless of whether it is merged on the remote or local system
# once merged, for cleaning, you must delete the local branch
git branch -D 25-my-new-functionality
Warning
NEVER reuse an already merged branch. If you are using the GitLab flow, which we strongly recommend always start every change from main and merge it back to main and if not, read about the Git flow the GitHub flow or the GitLab flow and choose a flow DO NOT invent your own flow, it will bring you many complications sooner or later.
To ensure that the semantic versions of the package are not contradicted by the git tags, you can take two paths: automate the git tags every time you change the version in the package, or automate the package version every time you tag git. Both ways have their advantages and disadvantages, but it is much better when the technology has already implemented it for you. For example, in the previous case, you may notice that a was never made git tag, and the reason is because the command npm version …takes care of synchronizing the version with a tag in git. This is an excellent evolution of development tools, previously, to keep the two synchronized you had to write code in the hooks of git, or of the tool (prerelease, postversion, etc.).
Docker
Versioning your images
Versioning with docker is a bit more complicated, since the Dockerfile (the text meta file that describes a container) does not have a version-specific field. The possibilities are to use a “label” (field supported by Docker) to put stamp a version. But, since a version-specific field is not natively supported by the docker environment, this is a case in which I recommend that the only source of truth regarding the version resides outside the file, and what better than in the tag of git. However, since there is no automatic syncing of the version to the tag, we must ensure that our pipeline takes care of enforcing such a restriction. Also, Docker adds a small change to the Semantic Versioning script.
According to SemVer, each different artifact must have a different major, minor and patch version, thus, the first version released will be 1.0.0, the next patch 1.0.1 and when new functionality is added it will be 1.1.0. Simple, but Docker adds that when releasing 1.0.0, it should be labeled 1, 1.0, and 1.0.0, when the first patch creates version 1.0.1, it should now also be labeled 1 and 1.0. This practice, although accepted in the docker environment, is not 100% compatible with SemVer since version 1.0 of yesterday has the possibility of being different from version 1.0 of today, even so, it is the accepted practice and you have to use it.
Date | Event | Major Label | Minor Label | SemVer Tag | Tag “latest” |
2022-01-01 | First version | 1 | 1.0 | 1.0.0 | latest |
2022-01-02 | Small fix | 1 | 1.0 | 1.0.1 | latest |
2022-01-03 | New functionality | 1 | 1.1 | 1.1.0 | latest |
To comply with SemVer and at the same time with docker best practices, the best place to generate all these tags is the pipeline. But, it is necessary to ensure that the company agrees to use a strict and documented process to avoid confusion.
My recommendation is that the internal tag is “development” by default, but that it can be modified during the construction of the image. Also, in case it is necessary to know the version of the image inside the container, it can be put in an environment variable (ENV).
# In the Dockerfile
ARG IMAGE_VERSION="development"
FROM alpine:latest
ENV IMAGE_VERSION=$VERSION
LABEL version="development"
# At build time
IMAGE_VERSION=$(git describe --abbrev=0)
docker build . --build-arg IMAGE_VERSION=$IMAGE_VERSION --label version="$IMAGE_VERSION" -t myregistry/myimage:$IMAGE_VERSION
Note that when building the image, the last git tag is used to build the image and put it in three places:
- As an argument (–build-arg), which will be used to create an environment variable that can be consumed from within the container
- As label (–label), which will be recorded as “meta” information of the container
- As a tag (-t), which will serve to register it in the corresponding image registry
Versioning the images you depend on
During the construction of your application’s image you will probably use your own images or third party images, it is important that you use your tags well to ensure that your application the image used is the correct one. As we saw earlier, docker recommends that each new version be labeled as latest, but that can be a blessing or a curse, depending on your needs and circumstances. When using latest, you have the confidence of always using the latest version, that is, without effort on your part, your application will always be up to date, that is the good part, however, as you can imagine, you will also be receiving all the new errors that have been introduced in new versions. For applications that run in production, it is necessary to have stability and therefore, it is not recommended that your application uses the latest tag. But, if you have created a mature pipeline, with enough gates to verify that your application is 100% functional before being put in production, then it’s fine to use latest, but very carefully. In the long run, wearing latest will probably bring you more headaches than blessings, and that’s why it’s not highly recommended.
When using base images for your image, be sure to use specific tags in your line FROM to avoid surprises, or at least not to vary the larger version.
# Good
FROM nginx:1.21.5
FROM nginx:1.21
FROM nginx:1
# Bad
FROM nginx
FROM nginx:latest
When using your own images or third party images in your application deployment (compose, helm, etc.), use specific tags.
spec:
containers:
- name: nginx
image: nginx:1.21.5
- name: myapp
image: registry.company.com/myapp:3.4.6
Pipeline
It is extremely important that artifacts created during the pipeline are stamped with the same version that accompanies the text files (source code) that generated the artifact. A great effort should be made that all this happens automatically and manual updates are avoided once the version has been updated in the source code. For example, if an application written in NodeJS is going to be published in a container, there is a great temptation to keep two versions, one within the application and the other for deployment. However, these two are the same, they should be kept in parallel and they should never go out of sync. So, if there is a problem in production, and the production version is 1.3.5, I know exactly which version of the code was used to generate that container. Let’s see, there are several places that need to be kept in sync:
- package.json – single source of truth
- container – when this container is built, it must reflect the same version of the application installed in it
- deployment – (eg helm) When deploying the application using templates, it is necessary to pass the container version to the template
The pipeline should read the application version from the application meta file and use it elsewhere. This is sometimes difficult because pipelines tend to be segregated into multiple steps and not all steps have access to the original application meta file. In those cases, there must be an initial step that extracts the version from the meta file and passes it as an environment variable to the other steps in the pipeline. For example, in GitLab CI an artifact is created with an .env file to pass the information to the other steps in the pipeline:
setup:
script:
- echo "APP_VERSION="$(node -p require('./package.json').version") >> build.env
artifacts:
reports:
dotenv: build.env
build:
dependencies:
- setup
script:
- docker build . --build-arg IMAGE_VERSION=$APP_VERSION --label version="$APP_VERSION" -t myregistry/myapp:$APP_VERSION
deploy:
dependencies:
- setup
script:
- RELEASE_NAME=myapp_$APP_VERSION
- helm upgrade --set image=$CI_REGISTRY_IMAGE --set tag=$APP_VERSION --install $RELEASE_NAME chartmuseum/myapp