Ensuring Version Consistency Across All Environments
Ensuring Version Consistency Across All Environments
by Dan Manges

Ensuring Version Consistency Across All Environments

As a best practice, engineering teams should ensure that the same version of tools and runtimes are used across all environments.

As tools and languages mature, they generally do a good job of maintaining backwards compatibility. However, sometimes subtle bugs can arise from version differences.

We recently ran into a bug in Mint because of a version difference in Node.

We were developing on version 20.12.2 and calling filehandle.read which takes four arguments: (buffer, offset, length, position).

In 20.12.2, offset, length, and position have default values and only buffer is required.

However, we had a piece of our infrastructure that was running on 20.11.1 instead of 20.12.2. In that version of Node, those parameters do not have default values, which resulted in an unexpected runtime error.

Thankfully we have a thorough acceptance test suite which caught the bug in staging before it made its way to production. But we would have rather caught this in development. It’s not enough to use the same major version – everything should be running the same minor and patch version as well.

The best way to enforce version consistency is checking for it explicitly in a CI pipeline.

Using .tool-versions

Ideally, you should have a single source of truth for the versions that you’re using. However, you may need to specify the version in multiple places. In that scenario, you should check for consistency among the disparate files in a CI task.

At RWX, our engineering team uses asdf for development, so we use a .tool-versions file as the primary source of truth. We then run a Mint task that extracts values from .tool-versions and also ensures consistency in other files that need to reference the respective versions.

If you're not familiar with .tool-versions, our file looks like this.

1
2
3
golang 1.22.2
nodejs 20.12.2
pnpm 8.15.8

And then here's our Mint task.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- key: tool-versions
  use: code
  run: |
    set -x
    grep '^nodejs ' .tool-versions | awk '{print $2}' | tee $MINT_VALUES/nodejs
    nodejs=$(cat $MINT_VALUES/nodejs)
    grep "nodejs-$nodejs-1" ami/task-server/provisioning/provision.sh
    grep "nodejs=$nodejs-1" bin/agent-devel/provisioning/provision.sh
    grep ""@types/node": "$nodejs"" package.json

    grep '^golang ' .tool-versions | awk '{print $2}' | tee $MINT_VALUES/golang
    golang=$(cat $MINT_VALUES/golang)
    grep "/go${golang}.linux-" bin/agent-devel/install-deps.sh

    grep '^pnpm ' .tool-versions | awk '{print $2}' | tee $MINT_VALUES/pnpm
    pnpm=$(cat $MINT_VALUES/pnpm)
    grep "pnpm@${pnpm}" bin/agent-devel/install-deps.sh
  filter:
    - .tool-versions
    - package.json
    - ami/task-server/provisioning/provision.sh
    - bin/agent-devel/provisioning/provision.sh
    - bin/agent-devel/install-deps.sh

The following lines extract the desired versions from .tool-versions and set the results as Mint values

1
2
3
grep '^nodejs ' .tool-versions | awk '{print $2}' | tee $MINT_VALUES/nodejs
grep '^golang ' .tool-versions | awk '{print $2}' | tee $MINT_VALUES/golang
grep '^pnpm ' .tool-versions | awk '{print $2}' | tee $MINT_VALUES/pnpm

We then use these values in the Mint tasks which install the respective languages or tools.

1
2
3
4
5
6
7
8
9
10
11
12
13
- key: node
  call: mint/install-node 1.0.6
  with:
    node-version: ${{ tasks.tool-versions.values.nodejs }}

- key: pnpm
  use: node
  run: npm install -g pnpm@${{ tasks.tool-versions.values.pnpm }}

- key: go
  call: mint/install-go 1.0.6
  with:
    go-version: ${{ tasks.tool-versions.values.golang }}

The grep lines make sure other files which reference the Node version are using the same version.

1
2
3
4
nodejs=$(cat $MINT_VALUES/nodejs)
grep "nodejs-$nodejs-1" ami/task-server/provisioning/provision.sh
grep "nodejs=$nodejs-1" bin/agent-devel/provisioning/provision.sh
grep ""@types/node": "$nodejs"" package.json

The lines which we're checking in the referenced files are:

1
sudo yum install nodejs-20.12.2-1nodesource --setopt=nodesource-nodejs.module_hotfixes=1 -y
1
sudo apt-get install -y nodejs=20.12.2-1nodesource1
1
2
3
"devDependencies": {
  "@types/node": "20.12.2"
}

Similarly the grep command for Go:

1
2
golang=$(cat $MINT_VALUES/golang)
grep "/go${golang}.linux-" bin/agent-devel/install-deps.sh

is checking this line:

1
curl -L https://go.dev/dl/go1.22.2.linux-amd64.tar.gz -o /tmp/go1.22.2.linux-amd64.tar.gz

And the grep command for pnpm:

1
2
pnpm=$(cat $MINT_VALUES/pnpm)
grep "pnpm@${pnpm}" bin/agent-devel/install-deps.sh

is checking this line:

1
sudo npm install -g [email protected]

The Mint task is fast to run, but it's also using a filter so that the majority of the time it will be a cache hit.

1
2
3
4
5
6
filter:
  - .tool-versions
  - package.json
  - ami/task-server/provisioning/provision.sh
  - bin/agent-devel/provisioning/provision.sh
  - bin/agent-devel/install-deps.sh

Read more on filtering files to produce cache hits in Mint.

Follow Along

We write a lot about software engineering best practices and CI/CD pipelines in Mint. Follow along on X at @rwx_research, LinkedIn, or our email newsletter

Enjoyed this post? Share it!