Ensuring Version Consistency Across All Environments

CI/CD
May 23, 2024
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

Never miss an update

Get the latest releases and news about RWX and our ecosystem with our newsletter.

Share this post

Enjoyed this post? Pleas share it on your favorite social network!

Related posts

Read more on updates and advice from the RWX engineering team

See all posts
More CPU and Memory for Tasks
CI/CD

More CPU and Memory for Tasks

RWX now supports running tasks with up to 64 CPUs and 256 GB of memory.

Sep 24, 2024
Read now
Adaptive CI Pipelines with Dynamic Tasks
CI/CD

Adaptive CI Pipelines with Dynamic Tasks

Dynamic tasks enable building CI pipelines which are adaptive based on things like the commit contents or data from external data sources. They unlock a lot of possibilities for building highly performant CI pipelines.

May 31, 2024
Read now
What to Do When Merging Faster than Your Continuous Deployment Process
CI/CD

What to Do When Merging Faster than Your Continuous Deployment Process

When multiple commits are merged in rapid succession, rather than creating a backlog of deployments, a reasonable solution is to skip deploying the earlier commits and only deploy the most recent one.

May 30, 2024
Read now