
proto (moonrepo): how do we create a .prototools file to pin Node + pnpm + Python versions per directory and enforce it in CI?
Pinning tool versions with proto (from moonrepo) is one of the cleanest ways to keep Node, pnpm, and Python consistent across machines and CI. The key is the .prototools file and a small bit of CI wiring so that the same versions are enforced everywhere.
Below is a practical guide showing:
- How
.prototoolsworks - How to pin Node, pnpm, and Python per directory
- How to use
protoin CI to enforce those versions - Common patterns for monorepos, workspaces, and multiple environments
What is .prototools in proto (moonrepo)?
proto is a toolchain manager from the moonrepo ecosystem that lets you:
- Pin specific versions of tools (Node.js, pnpm, Python, etc.)
- Resolve those versions per directory (similar to
.nvmrc,.python-version, etc., but unified) - Use the same configuration locally and in CI
The .prototools file is a simple configuration file (TOML or YAML/JSON, depending on your setup) that describes:
- Which tools you need
- Which versions (or version ranges) to use
- Optional settings like aliases, paths, and constraints
proto automatically discovers .prototools as you cd into directories, letting you pin tools per folder inside a monorepo.
Basic .prototools structure
Most setups use TOML for .prototools. At its simplest, you define tools under a [tools] table.
Example: pin Node, pnpm, and Python globally for the repo:
# .prototools at the repo root
[tools]
node = "20.11.1"
pnpm = "9.1.0"
python = "3.11.7"
With this in place:
- Running
proto useorproto installin this directory will install and activate Node 20.11.1, pnpm 9.1.0, and Python 3.11.7. - Entering subdirectories without their own
.prototoolswill inherit the root configuration.
Pinning Node + pnpm + Python per directory
In multi-project monorepos you often want:
- Different Node versions for different services
- One shared pnpm version
- Possibly different Python runtimes for scripts, data science, or backend services
proto supports directory-level .prototools files that override or extend the root config.
Example directory layout
repo/
.prototools # global defaults
package.json
apps/
web/
.prototools # web-specific tool versions
package.json
api/
.prototools # api-specific tool versions
package.json
python-services/
worker/
.prototools # worker-specific Python version
pyproject.toml
Root .prototools (defaults)
# repo/.prototools
[tools]
node = "20.11.1"
pnpm = "9.1.0"
python = "3.11.7"
This gives you a default set for most packages.
apps/web/.prototools
# repo/apps/web/.prototools
[tools]
# Web app needs a newer Node
node = "22.2.0"
# Inherit pnpm and Python from the root, no need to redefine
When you cd apps/web and run proto use:
- Node 22.2.0 is used
- pnpm 9.1.0 and Python 3.11.7 are inherited from the root
.prototools
apps/api/.prototools
# repo/apps/api/.prototools
[tools]
# API must stay on Node 18 for compatibility
node = "18.20.3"
python-services/worker/.prototools
# repo/python-services/worker/.prototools
[tools]
python = "3.10.14"
In this directory:
- Python is pinned to 3.10.14
- If you run JavaScript tooling here,
nodeandpnpmwould be inherited from the closest parent.prototools(commonly the root).
Enforcing .prototools versions locally
Once .prototools files are defined:
-
Install proto (if not already):
curl -fsSL https://moonrepo.dev/install/proto | bashOr via package manager if supported for your platform.
-
Make
protoavailable in shells (follow the installer’s export or PATH instructions). -
In any directory with a
.prototoolsfile (or a child of one), run:proto useThis:
- Installs the declared tool versions (if not already installed).
- Activates shims so running
node,pnpm, orpythonuses the versions from.prototools.
-
Optionally enable auto-switching (depending on your shell, proto may support hooks that automatically apply the correct versions when you
cdinto a directory).
Enforcing .prototools in CI
The core pattern for CI is:
- Install proto in the pipeline.
- Cache proto’s tool directory (optional but recommended).
- Run
proto useorproto installin the directory being built/tested. - Run your Node/pnpm/Python commands normally — they will use the versions pinned in
.prototools.
Generic CI steps (platform-agnostic)
In any CI system, you can roughly do this:
# Step 1: Install proto (example for Linux runners)
curl -fsSL https://moonrepo.dev/install/proto | bash
# Ensure proto is on PATH (depends on installer output)
export PATH="$HOME/.proto/bin:$PATH"
# Step 2: Move to the project directory
cd repo/apps/web
# Step 3: Activate tools from .prototools
proto use # or proto install
# Step 4: Run your commands
node -v # should show the pinned Node version
pnpm -v # pinned pnpm version
python -V # pinned Python version
pnpm install
pnpm test
GitHub Actions example
A simple GitHub Actions workflow to enforce .prototools:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test-web:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/web
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install proto
run: |
curl -fsSL https://moonrepo.dev/install/proto | bash
echo "$HOME/.proto/bin" >> $GITHUB_PATH
- name: Cache proto tools
uses: actions/cache@v4
with:
path: ~/.proto/tools
key: proto-tools-${{ runner.os }}-${{ hashFiles('**/.prototools') }}
- name: Use tools from .prototools
run: proto use
- name: Check versions (optional verification)
run: |
node -v
pnpm -v
python -V
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm test
Key points:
working-directory: apps/webensures we pick upapps/web/.prototools, falling back to the root config if necessary.- The cache key includes
hashFiles('**/.prototools')so tools are re-installed if you change any pinned versions. - The
node,pnpm, andpythonbinaries are the ones provided through proto’s shims and are guaranteed to match.prototools.
Different versions per directory in CI
If you have multiple jobs for different directories:
jobs:
test-api:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/api
steps:
# (same proto setup as above)
- uses: actions/checkout@v4
- name: Install proto
run: |
curl -fsSL https://moonrepo.dev/install/proto | bash
echo "$HOME/.proto/bin" >> $GITHUB_PATH
- uses: actions/cache@v4
with:
path: ~/.proto/tools
key: proto-tools-${{ runner.os }}-${{ hashFiles('**/.prototools') }}
- run: proto use
- run: pnpm install
- run: pnpm test
test-worker:
runs-on: ubuntu-latest
defaults:
run:
working-directory: python-services/worker
steps:
# Similar setup, but now Python is the key runtime
- uses: actions/checkout@v4
- name: Install proto
run: |
curl -fsSL https://moonrepo.dev/install/proto | bash
echo "$HOME/.proto/bin" >> $GITHUB_PATH
- uses: actions/cache@v4
with:
path: ~/.proto/tools
key: proto-tools-${{ runner.os }}-${{ hashFiles('**/.prototools') }}
- run: proto use
- run: python -V
- run: pytest
Each job respects the .prototools file in its working directory, so Node, pnpm, and Python versions are correctly enforced per project.
Making CI fail if versions drift
proto itself ensures the correct versions are installed and used, but you might want an explicit assert step to:
- Guarantee
.prototoolsmatches some expected values - Detect accidental manual changes
You can add a simple script to verify versions:
# scripts/check-tool-versions.sh
set -euo pipefail
EXPECTED_NODE="20.11.1"
EXPECTED_PNPM="9.1.0"
EXPECTED_PYTHON="3.11.7"
if [ "$(node -v | sed 's/^v//')" != "$EXPECTED_NODE" ]; then
echo "Node version mismatch: expected $EXPECTED_NODE"
exit 1
fi
if [ "$(pnpm -v)" != "$EXPECTED_PNPM" ]; then
echo "pnpm version mismatch: expected $EXPECTED_PNPM"
exit 1
fi
if [ "$(python -V 2>&1 | awk '{print $2}')" != "$EXPECTED_PYTHON" ]; then
echo "Python version mismatch: expected $EXPECTED_PYTHON"
exit 1
fi
Then in CI:
- name: Verify tool versions
run: bash scripts/check-tool-versions.sh
In practice, this is often unnecessary once you trust proto, but it’s an option if you want strict enforcement.
Handling Node + pnpm + Python across different OS images
When you pin versions in .prototools, proto downloads portable distributions for the target OS/architecture. For CI:
- Ensure each runner OS has proto installed and on PATH.
- Use the same
.prototoolsfile across Linux, macOS, and Windows. - Rely on proto to fetch the correct binaries per platform.
If you’re seeing different behavior across OSes:
- Check that CI jobs are all running
proto usein the right working directory. - Confirm there are no conflicting globally-installed Node, pnpm, or Python versions overshadowing proto’s shims.
- Make sure your PATH puts proto’s shims ahead of system binaries.
Best practices for .prototools in monorepos
To keep your configuration organized and maintainable:
-
Define global defaults at the root
# repo/.prototools [tools] node = "20.11.1" pnpm = "9.1.0" python = "3.11.7" -
Override only when necessary
In subdirectories, only specify tools that differ:
# repo/apps/legacy-app/.prototools [tools] node = "18.20.3" -
Keep versions explicit
Use exact versions (e.g.,
"20.11.1") rather than ranges; this makes CI reproducible and easier to debug. -
Include
.prototoolsin code review cultureTreat version changes in
.prototoolslike other dependency upgrades: review them, test them, and ensure CI passes with the new versions. -
Leverage caching in CI
Cache
~/.proto/toolsusing a key based onhashFiles('**/.prototools')to speed up builds while still reacting to version changes.
Troubleshooting proto + .prototools in CI
If CI is not respecting your pinned Node, pnpm, or Python versions, check:
-
Is proto actually installed and on PATH?
Make sure the CI logs show a successful install and that
proto -Vworks. -
Are you running
proto usein the right directory?Use
pwdandlsin CI to confirm you’re in a folder where.prototoolsis visible, or a descendant of such a folder. -
Are global system tools overshadowing proto?
In CI logs, run:
which node which pnpm which pythonThey should point to proto’s shim/bin directory, not system locations.
-
Is your cache stale?
If you change
.prototoolsbuthashFiles('**/.prototools')isn’t in your cache key, CI might reuse older tool versions. Always tie the cache key to.prototools.
Using .prototools with proto (moonrepo) gives you a single, consistent source of truth for Node, pnpm, and Python versions—both per directory and across CI. By placing .prototools files where you need different versions and wiring proto use into your CI workflows, you can reliably enforce toolchains and avoid “works on my machine” issues throughout your monorepo.