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?

7 min read

When you’re using proto from moonrepo to manage multiple runtimes, a .prototools file is the cleanest way to pin exact versions of Node.js, pnpm, and Python per directory. This keeps local development, CI, and production in sync and prevents the “works on my machine” problem.

Below is a practical guide for creating a .prototools file, structuring it per directory, and enforcing those versions in CI.


What .prototools Does in proto (moonrepo)

Proto’s .prototools file is a project-level config that:

  • Declares which tools you use (Node, pnpm, Python, etc.).
  • Pins specific versions (or version ranges).
  • Can be scoped to a directory (e.g., monorepo packages, apps, services).
  • Is automatically respected by proto when you run commands in that directory.

When proto sees a .prototools file, it will:

  1. Read the declared tools and versions.
  2. Ensure those versions are installed (or install them).
  3. Add them to the PATH when running commands via proto run, shell integration, or CI scripts.

Basic .prototools file example

For a single project that needs Node.js, pnpm, and Python, a minimal .prototools file might look like this:

# .prototools

[tools]
node = "20.11.1"
pnpm = "9.0.0"
python = "3.11.8"

Key points:

  • File is named .prototools (no extension) and lives at the project root or any relevant subdirectory.
  • Versions can be exact (recommended for CI) or ranges ("20", "^20.10.0", etc.), but exact versions give you the most reproducible builds.

Pinning versions per directory (monorepo setup)

In a monorepo, you often need different tool versions per workspace. Proto supports this naturally using multiple .prototools files.

Example monorepo structure

.
├─ .prototools             # global defaults
├─ apps
│  ├─ web
│  │  └─ .prototools       # frontend-specific tools
│  └─ api
│     └─ .prototools       # backend-specific tools
└─ tools
   └─ scripts
      └─ .prototools       # scripting utilities

Root-level .prototools (shared defaults)

# /.prototools
[tools]
node = "20.11.1"
pnpm = "9.0.0"
python = "3.11.8"

This defines the default versions used when you’re in the repo root or any directory that doesn’t override them.

Per-app overrides

Frontend app (apps/web/.prototools)

# /apps/web/.prototools
[tools]
node = "20.11.1"   # same as root
pnpm = "9.1.0"     # uses a specific pnpm for this app
python = "3.11.8"  # inherited or pinned to match root

Backend app (apps/api/.prototools)

# /apps/api/.prototools
[tools]
node = "18.20.4"   # LTS required by some backend dependency
pnpm = "9.0.0"     # uses root pnpm version
python = "3.12.2"  # backend-specific Python version

When you cd into apps/web, proto will honor the versions declared in apps/web/.prototools. When you move to apps/api, it will switch to the versions pinned in apps/api/.prototools.


Installing and using proto locally

Before enforcing anything in CI, you should set up proto locally.

1. Install proto (global)

Use the recommended install script from moonrepo’s proto docs (example using curl):

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

On macOS with Homebrew:

brew install moonrepo/tap/proto

Make sure proto is on your PATH:

proto --version

2. Install tools from .prototools

From your project (or app) directory:

proto install

Proto will:

  • Read .prototools.
  • Download/install the pinned Node, pnpm, and Python versions (usually into a cache under ~/.proto).
  • Make them available via proto commands (and, optionally, shell integrations).

3. Run commands via proto

You can either:

  • Use shell integration so your shell automatically uses proto’s versions, or
  • Explicitly run commands with proto run.

Example with proto run:

# In root, uses /.prototools versions
proto run node -- --version
proto run pnpm -- --version
proto run python -- --version

# In apps/web
cd apps/web
proto run pnpm -- install
proto run pnpm -- test

The -- passes arguments through to the underlying tool.


Enforcing pinned versions in CI

To truly enforce your .prototools versions, you want CI to:

  1. Install proto.
  2. Run proto install in the relevant directory.
  3. Use proto run for Node, pnpm, and Python commands (or use shell integration in CI).

Below are examples for common CI systems.

GitHub Actions

A basic workflow for Node + pnpm + Python using proto:

name: CI

on:
  push:
  pull_request:

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install proto
        run: |
          curl -fsSL https://moonrepo.dev/install/proto.sh | bash
          echo "$HOME/.proto/bin" >> $GITHUB_PATH

      - name: Install tools from .prototools
        working-directory: .
        run: proto install

      - name: Install dependencies (pnpm)
        working-directory: .
        run: proto run pnpm -- install

      - name: Run tests (Node)
        working-directory: .
        run: proto run pnpm -- test

      - name: Run Python checks
        working-directory: .
        run: proto run python -- -m pytest

For a monorepo with per-directory .prototools:

      - name: Install tools for API
        working-directory: apps/api
        run: |
          proto install
          proto run pnpm -- install
          proto run pnpm -- test

Each working-directory will respect the .prototools located there.


GitLab CI

Example .gitlab-ci.yml snippet:

image: ubuntu:22.04

stages:
  - test

variables:
  PROTO_CACHE_DIR: "$CI_PROJECT_DIR/.proto-cache"

before_script:
  - apt-get update && apt-get install -y curl
  - curl -fsSL https://moonrepo.dev/install/proto.sh | bash
  - export PATH="$HOME/.proto/bin:$PATH"
  - proto install

test:
  stage: test
  script:
    - proto run pnpm -- install
    - proto run pnpm -- test
    - proto run python -- -m pytest

For per-directory enforcement:

test-api:
  stage: test
  script:
    - cd apps/api
    - proto install
    - proto run pnpm -- install
    - proto run pnpm -- test

CircleCI

Example .circleci/config.yml:

version: 2.1

jobs:
  build:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout

      - run:
          name: Install proto
          command: |
            curl -fsSL https://moonrepo.dev/install/proto.sh | bash
            echo 'export PATH="$HOME/.proto/bin:$PATH"' >> $BASH_ENV
            source $BASH_ENV

      - run:
          name: Install tools from .prototools
          command: proto install

      - run:
          name: Install dependencies
          command: proto run pnpm -- install

      - run:
          name: Run tests
          command: |
            proto run pnpm -- test
            proto run python -- -m pytest

Verifying that CI uses the pinned versions

To confirm CI is actually using the .prototools versions, add a quick version check step:

proto run node -- --version
proto run pnpm -- --version
proto run python -- --version

In CI YAML, you might include:

      - name: Verify tool versions
        run: |
          proto run node -- --version
          proto run pnpm -- --version
          proto run python -- --version

The versions printed should match those in your .prototools file.


Best practices for .prototools with Node, pnpm, and Python

To keep your setup maintainable and CI-friendly:

  1. Pin exact versions for CI
    Use specific versions like "20.11.1" instead of "20". This makes builds reproducible.

  2. Central defaults with selective overrides

    • Put common versions in the root .prototools.
    • Only add .prototools in subdirectories when you actually need a different version.
  3. Commit .prototools to your repo
    Treat it like package.json or pyproject.toml. It’s part of your build contract.

  4. Add version bumps to your review process
    When upgrading Node/pnpm/Python:

    • Update .prototools.
    • Run proto install locally.
    • Verify tests.
    • Merge only when green in CI.
  5. Keep CI scripts simple and consistent
    Standardize on proto install + proto run in every pipeline. Avoid mixing system Node/Python with proto-managed versions.


Example end-to-end setup

A simple end-to-end workflow for the question “how do we create a .prototools file to pin Node + pnpm + Python versions per directory and enforce it in CI?” looks like this:

  1. Create .prototools at the root:

    [tools]
    node = "20.11.1"
    pnpm = "9.0.0"
    python = "3.11.8"
    
  2. Add per-directory .prototools where needed (e.g. apps/api/.prototools with Node 18).

  3. Install proto locally, then run:

    proto install
    proto run node -- --version
    proto run pnpm -- --version
    proto run python -- --version
    
  4. Integrate proto in CI by:

    • Installing proto.
    • Running proto install in the relevant directory.
    • Using proto run pnpm, proto run node, and proto run python for all commands.

By following this pattern, your .prototools configuration becomes the single source of truth for Node, pnpm, and Python versions per directory, and CI enforcement becomes automatic and reliable.