logo
Navigate back to the homepage

Versioning JavaScript Apps

Kamlesh Chandnani
May 26th, 2020 · 7 min read

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 syntax
2- name: Install Dependencies
3 run: yarn
4- name: Run tests
5 run: yarn test
6- name: Build app
7 run: yarn build
8- name: Bump the version
9 run: npm version patch
10- name: Publish to npm
11 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:

  1. 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.
  2. Imagine someone saying the current version of the application is c71802c8 and we want to rollback to 9f9288bc.

Doesn’t it look and sound gibberish 🤮? To me yes it does!

Let’s look at another meaningful way to convey the same information:

  1. 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 be v0.0.2. The other person will automatically understand(assuming people understand semver) that this release is going to be some bug fixes.
  2. Imagine someone saying the current version of the application is v0.0.2 and we want to rollback to v0.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:

  1. Bump the version in CI(patch, minor, major)
  2. Build the application
  3. Commit the updated version back to git
  4. Push git tags with the same version in package.json - This is basically aliasing your commits with your package.json’s version and this is what npm version does as internally - read here. This will help you navigate quickly when someone asks you “Hey what was deployed in v0.0.1?” And you’ll be able to just jump to that version in git since it’s in sync with your package.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 Bump
4 runs-on: ubuntu-latest
5 steps:
6 - name: Checkout Codebase
7 uses: actions/checkout@v2
8 - name: Use Node v12
9 uses: actions/setup-node@v1
10 with:
11 node-version: 12
12 - name: Install Dependencies
13 run: yarn --frozen-lockfile
14 - name: Run Tests
15 run: yarn test
16 - name: Build Application
17 run: yarn production:build
18 - name: Bump package.json Version # We'll bump the version first
19 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 patch
25 - name: Upload Version Artifact # upload the updated package.json to artifacts, don't yet publish it.
26 uses: actions/upload-artifact@v2
27 with:
28 name: metadata
29 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 Build
3 runs-on: ubuntu-latest
4 needs: version-bump
5 steps:
6 - name: Checkout Codebase
7 uses: actions/checkout@v2
8 # we are downloading the artifact we uploaded in the job above so we can use the latest version number while building our application
9 - name: Download Version Artifact
10 uses: actions/download-artifact@v1
11 with:
12 name: metadata
13 # 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 image
14 - name: Set Version
15 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.json
18 echo "::set-env name=VERSION::$VERSION"
19 - name: Build Docker Image
20 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 Hub
26 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 Publish
3 runs-on: ubuntu-latest
4 # 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 job
5 needs: [staging-build]
6 steps:
7 - name: Checkout Codebase
8 uses: actions/checkout@v2
9 - name: Use Node v12
10 uses: actions/setup-node@v1
11 with:
12 node-version: 12
13 - name: Bump package.json Version
14 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 tag
21 - name: Publish Version
22 run: |
23 git push
24 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 🥁

release

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 😀

Join the Newsletter

Subscribe to get the latest write-ups about my learnings from JavaScript, React, Design Systems and Life

No spam, pinky promise!

More articles from Kamlesh Chandnani

Unpolished Thoughts from Deep Work

Notes I took while listening to Deep Work

May 12th, 2020 · 6 min read

Static vs Dynamic Environments

Environment variables allows us to customize the behavior of our application in different environments.

May 4th, 2020 · 5 min read
Link to https://github.com/kamleshchandnaniLink to https://twitter.com/@_kamlesh_Link to https://www.youtube.com/playlist?list=PLpATFO7gaFGgwZRziAoScNoAUyyR_irFMLink to https://linkedin.com/in/kamleshchandnani