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

What would GitHub Actions look like if you designed it today?
GHA was designed in ~2018. What would it look like if you designed it today, with all we know now?

Truly continuous integration: ensuring pull requests from agents have passing builds
RWX CLI v3.0.0 introduces new tools for developers and coding agents to iterate on changes until CI passes - without a single commit or push.

