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 RWX 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 an RWX 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 3golang 1.22.2 nodejs 20.12.2 pnpm 8.15.8
And then here's our RWX 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 $RWX_VALUES/nodejs nodejs=$(cat $RWX_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 $RWX_VALUES/golang golang=$(cat $RWX_VALUES/golang) grep "/go${golang}.linux-" bin/agent-devel/install-deps.sh grep '^pnpm ' .tool-versions | awk '{print $2}' | tee $RWX_VALUES/pnpm pnpm=$(cat $RWX_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 RWX values
1 2 3grep '^nodejs ' .tool-versions | awk '{print $2}' | tee $RWX_VALUES/nodejs grep '^golang ' .tool-versions | awk '{print $2}' | tee $RWX_VALUES/golang grep '^pnpm ' .tool-versions | awk '{print $2}' | tee $RWX_VALUES/pnpm
We then use these values in the RWX tasks which install the respective languages or tools.
1 2 3 4 5 6 7 8 9 10 11 12 13- key: node call: nodejs/install 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: golang/install 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 4nodejs=$(cat $RWX_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:
1sudo yum install nodejs-20.12.2-1nodesource --setopt=nodesource-nodejs.module_hotfixes=1 -y
1sudo 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 2golang=$(cat $RWX_VALUES/golang) grep "/go${golang}.linux-" bin/agent-devel/install-deps.sh
is checking this line:
1curl -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 2pnpm=$(cat $RWX_VALUES/pnpm) grep "pnpm@${pnpm}" bin/agent-devel/install-deps.sh
is checking this line:
1sudo npm install -g pnpm@8.15.8
The RWX 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 6filter: - .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 RWX.
#Follow Along
We write a lot about software engineering best practices and CI/CD pipelines in RWX. Follow along on X at @rwx_research, LinkedIn, or our email newsletter
Related posts

RWX November 2025 Recap: container image builds, git patching runs, OTEL, and more
At RWX, we use our own product to rapidly prototype, develop, and ship features all the time. Here's what we've built recently...

We deleted our Dockerfiles: a better, faster way to build container images
Two weeks ago, we deleted the Dockerfile for our application, and we deleted the step in our CI pipelines that previously ran docker build.

