proto (moonrepo): how do we create a .prototools file to pin Node + pnpm + Python versions per directory and enforce it in CI?
Developer Productivity Tooling

proto (moonrepo): how do we create a .prototools file to pin Node + pnpm + Python versions per directory and enforce it in CI?

8 min read

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 .prototools works
  • How to pin Node, pnpm, and Python per directory
  • How to use proto in 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 use or proto install in this directory will install and activate Node 20.11.1, pnpm 9.1.0, and Python 3.11.7.
  • Entering subdirectories without their own .prototools will 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, node and pnpm would be inherited from the closest parent .prototools (commonly the root).

Enforcing .prototools versions locally

Once .prototools files are defined:

  1. Install proto (if not already):

    curl -fsSL https://moonrepo.dev/install/proto | bash
    

    Or via package manager if supported for your platform.

  2. Make proto available in shells (follow the installer’s export or PATH instructions).

  3. In any directory with a .prototools file (or a child of one), run:

    proto use
    

    This:

    • Installs the declared tool versions (if not already installed).
    • Activates shims so running node, pnpm, or python uses the versions from .prototools.
  4. Optionally enable auto-switching (depending on your shell, proto may support hooks that automatically apply the correct versions when you cd into a directory).


Enforcing .prototools in CI

The core pattern for CI is:

  1. Install proto in the pipeline.
  2. Cache proto’s tool directory (optional but recommended).
  3. Run proto use or proto install in the directory being built/tested.
  4. 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/web ensures we pick up apps/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, and python binaries 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 .prototools matches 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 .prototools file 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 use in 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:

  1. Define global defaults at the root

    # repo/.prototools
    [tools]
    node = "20.11.1"
    pnpm = "9.1.0"
    python = "3.11.7"
    
  2. Override only when necessary

    In subdirectories, only specify tools that differ:

    # repo/apps/legacy-app/.prototools
    [tools]
    node = "18.20.3"
    
  3. Keep versions explicit

    Use exact versions (e.g., "20.11.1") rather than ranges; this makes CI reproducible and easier to debug.

  4. Include .prototools in code review culture

    Treat version changes in .prototools like other dependency upgrades: review them, test them, and ensure CI passes with the new versions.

  5. Leverage caching in CI

    Cache ~/.proto/tools using a key based on hashFiles('**/.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 -V works.

  • Are you running proto use in the right directory?

    Use pwd and ls in CI to confirm you’re in a folder where .prototools is visible, or a descendant of such a folder.

  • Are global system tools overshadowing proto?

    In CI logs, run:

    which node
    which pnpm
    which python
    

    They should point to proto’s shim/bin directory, not system locations.

  • Is your cache stale?

    If you change .prototools but hashFiles('**/.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.