Versioning applications is important during the software development lifecycle. It defines a snapshot of your application at any given point of time. Your software is like a kid that’s growing and you measure their age with a number and basis that their maturity is derived. 😃
What is a Version? 🤔
It’s a unique identifier that defines the state of your application at any given point of time. For example Git commit SHA, package.json
’s version
field or it can be any custom identifier.
Concerns around versioning? 🙄
I was reading about what are different strategies around versioning, how do different software developers version their applications, when to version - during build time or during deploy time, where to version - CI or CD or locally etc. But I kind of struggled to find out exact answers. Most of the posts I read mentioned that versioning is a manual process and only the software developer can decide whether it’s a bug fix, feature or a breaking change and basis that they should bump the versions of their application. I wasn’t quite convinced because we live in this era where machines can do most of our mundane tasks with smartness. So why not versioning as well? Hence, I decided to explore this path to figure out some automated versioning strategies for my own projects.
I’ll be considering semantic versioning as the versioning strategy throughout the article. If you’re unaware about semantic versioning, you can read about it here.
When to Bump Version? 🤔
Typically most of the software have a Continuous Integrations(CI) and then Continuous Deployment(CD) process so now the confusion arises where to bump the version? Shall it be part of the CI or shall it be part of the CD?
To answer this question I would like to divide the applications based on their nature into 2 categories/types:
1. Libraries
These are basically packages that we publish to npm/any registries for other people to use via npm install
or yarn add
. NPM enforces semver as the versioning strategy so the consumers of your library can make a conscious choice about when to update the library version. So what you do is install dependencies, run tests, bump the version and publish to npm
1# GitHub actions syntax2- name: Install Dependencies3 run: yarn4- name: Run tests5 run: yarn test6- name: Build app7 run: yarn build8- name: Bump the version9 run: npm version patch10- name: Publish to npm11 run: npm publish
When we talk about versioning libraries things are pretty much straight forward mostly because you don’t have a CD here. What you have is a CI service - GitHub actions, CircleCI, Travis CI etc. so which means your versioning and publishing happens mostly at the same time and in the same process.
P.S: For the sake of simplicity I wrote npm version patch
which is a contrived example here. Taking this decision can also be automated using semantic-release
. I’ll talk about that as well in this post in a while
2. Applications
Applications are typically the products that you build and release it to servers for your consumers to use. For example: Kubernetes cluster, Lambda, CDN, S3 bucket, AWS EC2 machines, play store, app store etc.
Versioning gets tricky here since you now have a CI process where the application is built and then you have a CD process where your application is actually released on the servers after which your consumers will get the new version. So where do you bump the version? During CI? During CD?
The obvious answer will be that it should happen during CD since that’s when the application is released right? Look at the next section for details
Versioning at Build Time(CI) Vs Deploy Time(CD)? 🤷🏻♂️
So as we saw above versioning can be done during CI or CD but which one to choose?
We saw versioning during CI when we were discussing about libraries example above which is very straight forward. Now, let’s take an example of versioning during CD(Assuming we use the package.json
’s version
field)
Initial version = v0.0.1
Step-1: CI - build the app, get the version from package.json
and push to Docker with the version tag staging-v0.0.1
Step-2: CD (different process running at completely different) - pull the docker image staging-v0.0.1
, deploy to a server, bump package.json
version and commit the package.json
with the updated version to git
The version in package.json
now becomes v0.0.2
but wait the version that is deployed on the server was v0.0.1
right because we bumped the version after we deployed which means the version that is deployed vs the package.json
version are now out of sync 😱. Imagine if you have to rollback how difficult it is to identify since you’ll rollback to v0.0.1
but ideally that’s the version which is deployed which means the rollback won’t happen and you’ll keep scratching your head what went wrong 🤦♂️
So, shall we assume that versioning during CI is the only answer?
It’s not about version during CI or CD. Let me explain about the strategy and what we are trying to achieve.
Firstly, our goal is to identify the versions of our applications uniquely be it libraries or be it product applications. So which means whenever we merge any branch into master we build our application as part of CI so we can bump the version as part of this process itself regardless whether it is a library that will be published to the registry (npm) or an application that will be deployed to servers.
Now the trick here is where most of us confuse things is that when you say merged to master doesn’t mean it’s deployed to servers. It just means things which are merged to master are tested, stable and ready to be deployed at any point of time and guarantees that it wouldn’t break anything.
TL;DR: Bump version during CI and during deploy just deploy a particular version snapshot to servers.
P.S. It’s a recommendation or my approach not a rule so you can form your rules based on your use case
How to Version?
So, we have figured out when to bump the version let’s dive into the approaches and different ways to create these unique version identifiers.
The most common and widely used way of unique identifiers is to use Git commit SHA since they are the most unique identifiers and they also define the version/snapshot of your software at any point of time without you doing much of a hassle. It’s like versioning given to you out of the box. But there are some concerns that I see when referring to Git commit SHA as identifiers:
- Imagine someone asking you what version of our application is deployed right now or what is the next version we want to deploy? And we’ll be like
c71802c8
. - Imagine someone saying the current version of the application is
c71802c8
and we want to rollback to9f9288bc
.
Doesn’t it look and sound gibberish 🤮? To me yes it does!
Let’s look at another meaningful way to convey the same information:
- Imagine someone asking you what version of our application is deployed right now or what is the next version we want to deploy? And we say the current deployed version is
v0.0.1
and the next version we are going to deploy is going to bev0.0.2
. The other person will automatically understand(assuming people understand semver) that this release is going to be some bug fixes. - Imagine someone saying the current version of the application is
v0.0.2
and we want to rollback tov0.0.1
.
This just eases the communication and adds meaning to every release by default. On top of that it also self describes what type of release will this be - bug fixes(patch), releases(minor), major revamp/refactor(major).
Now in order to achieve this sort of versioning v0.0.1
what shall we actually do? Nothing much, we already have this information already in our package.json
’s version
field. We can just use that and make it a single source of truth. Here’s what the typical process looks like:
- Bump the version in CI(patch, minor, major)
- Build the application
- Commit the updated version back to git
- Push git tags with the same version in
package.json
- This is basically aliasing your commits with yourpackage.json
’s version and this is whatnpm version
does as internally - read here. This will help you navigate quickly when someone asks you “Hey what was deployed inv0.0.1
?” And you’ll be able to just jump to that version in git since it’s in sync with yourpackage.json
’s version field.
P.S This is again assuming that you follow semver and people in your team understand semver
Let’s see this in real? I’ll be refering this GitHub repository to demonstrate the whole process listed above.
If you’re not familiar with GitHub actions maybe you can read my post on it — Jumping down the rabbit hole of GitHub Actions
We’ll try to version a simple express application(it can be any JS application) whenever there’s a push to master. Assume you have this 👇🏻 GitHub action that runs on push event to master. So we’ll first bump the version(we won’t publish the version yet).
1jobs:2 version-bump:3 name: Version Bump4 runs-on: ubuntu-latest5 steps:6 - name: Checkout Codebase7 uses: actions/checkout@v28 - name: Use Node v129 uses: actions/setup-node@v110 with:11 node-version: 1212 - name: Install Dependencies13 run: yarn --frozen-lockfile14 - name: Run Tests15 run: yarn test16 - name: Build Application17 run: yarn production:build18 - name: Bump package.json Version # We'll bump the version first19 env:20 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}21 run: |22 git config --global user.email "kamlesh.chandnani@gmail.com"23 git config --global user.name "kamleshchandnani"24 npm version patch25 - name: Upload Version Artifact # upload the updated package.json to artifacts, don't yet publish it.26 uses: actions/upload-artifact@v227 with:28 name: metadata29 path: package.json
- In the above step we have basically bumped the version and uploaded the modified
package.json
to GitHub artifacts(think about this as temporary storage from GitHub). - We haven’t pushed the updated version and tags to git yet since we haven’t built our application and there are high chances that our application’s build process might fail so we shouldn’t release the version until the application is successfully built.
- In the next step we’ll build our application and push to docker
1staging-build:2 name: Staging Build3 runs-on: ubuntu-latest4 needs: version-bump5 steps:6 - name: Checkout Codebase7 uses: actions/checkout@v28 # we are downloading the artifact we uploaded in the job above so we can use the latest version number while building our application9 - name: Download Version Artifact10 uses: actions/download-artifact@v111 with:12 name: metadata13 # we will read the package.json's version field and set it into VERSION environment variable so we can use the same number to tag our Docker image14 - name: Set Version15 run: |16 VERSION=staging-v$(cat metadata/package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')17 cat metadata/package.json > package.json18 echo "::set-env name=VERSION::$VERSION"19 - name: Build Docker Image20 run: |21 docker build . \22 --file ./Dockerfile \23 --build-arg STAGE=staging \24 --tag ${DOCKER_HUB_REGISTRY}/${APP_NAME}:${VERSION}25 - name: Push to Docker Hub26 run: |27 docker login ${DOCKER_HUB_REGISTRY} --username ${DOCKER_HUB_USERNAME} --password ${{ secrets.GITHUB_TOKEN }}28 docker push ${DOCKER_HUB_REGISTRY}/${APP_NAME}:${VERSION}
- Now if our build step passes we need to publish the updated version number and tags to git
1version-publish:2 name: Version Publish3 runs-on: ubuntu-latest4 # all the jobs in GitHub actions run in parallel so with this option we are telling it to wait unitl the staging-build job is passed before we run the publish job5 needs: [staging-build]6 steps:7 - name: Checkout Codebase8 uses: actions/checkout@v29 - name: Use Node v1210 uses: actions/setup-node@v111 with:12 node-version: 1213 - name: Bump package.json Version14 env:15 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}16 run: |17 git config --global user.email "kamlesh.chandnani@gmail.com"18 git config --global user.name "kamleshchandnani"19 npm version patch -nm "chore(release): update version to %s"20 # this is where we are pushing the package.json with updated version to git as well as using the same version as the git tag21 - name: Publish Version22 run: |23 git push24 git push --tags
- After the above job is run we’ll have a new version of our application pushed to git along with tags and updated package.json version 🥁
TL;DR 📝
- Since JS applications are static bundles in the end, IMO Versioning should happen when we are building the applications.
- It’s not mandatory that every version we build should be deployed.
- If things are merged to master doesn’t mean they are deployed.
- Make version numbers easy to read and communicated. If we use named/meaningful version tags all the communication becomes easy and meaningful contrast to Git commit SHA.
- The ultimate goal when it comes to versioning is that we should have one unique identifier for our application that can then be used for all types of communications. For example your servers to identify what version is currently deployed, in monitoring tools to identify what version of the application is causing issues, to your support team to take complaints from the customers asking them to tell the version of the application they are seeing on their UI.
Phew! That was a wild ride ⚡️. I hope you learned something out of this.
If you have some thoughts around versioning or have handled versioning in different ways at work or side projects then you can write it to me or you can DM me on Twitter. I would love to hear them!
P.S. If you like this, make sure to subscribe, share this with your friends and follow me on twitter 😀