How-to

Pin the Salesforce CLI version in CI

A pipeline that installs the Salesforce CLI with no version is a pipeline that reinstalls a different program every week. Nothing in your repository changed; the build broke anyway. Pinning the version is fifteen minutes of work and removes an entire category of Monday morning.

Short version

Install an exact version — npm install --global @salesforce/cli@2.143.6, not @salesforce/cli — hold that number in one place, and assert the resolved version before the build does any work. Nimbus can do all three: nimbus toolchain sf install --version and nimbus toolchain sf status, with the expected version declared in nimbus.properties.

The problem

The Salesforce CLI ships on a fast cadence, and most CI configurations ask for whatever is newest:

yaml
# The line in almost every Salesforce pipeline
- run: npm install --global @salesforce/cli

That line means the toolchain is a moving target. Flags get deprecated, output shapes change, JSON keys move, a plugin stops being bundled, a bug ships and is fixed three days later. Meanwhile your build failed on a commit that touched a Visualforce page, and the first hour of debugging goes to the wrong place entirely, because "the CLI changed underneath me" is nobody's first hypothesis.

The second-order problem is worse: local machines and CI runners drift apart. "Works on my machine" becomes literally true and completely unhelpful.

The generic fix

Pin an exact version wherever the CLI is installed, and treat that number as configuration rather than something typed inline in five different workflow files.

bash
# Exact version, not "latest"
npm install --global @salesforce/cli@2.143.6

# What is actually resolved on this machine?
sf --version

Three ways to hold the number, in increasing order of durability:

  • A CI variable. One SF_VERSION repository or group variable, referenced by every workflow. Cheapest option, and enough for most teams.
  • A devDependency plus a lockfile. Add @salesforce/cli to the project's package.json and commit the lockfile. Now the version is versioned with the code, moves through review, and shows up in git log when it changes.
  • A container image. Bake the pinned CLI into the CI image. Fastest startup, strongest guarantee, most infrastructure to own.

Whichever you pick, add the assertion. An install step that silently succeeds with the wrong version is only marginally better than no pin at all.

The Nimbus way

Nimbus's local loop — test, validate, exec, lsp, the daemon — needs no Salesforce CLI at all. Remote work does: nimbus sf, sync, deploy, release and --fetch-missing all drive the official CLI, which Nimbus locates on demand. So Nimbus has to have an opinion about which one it found, and it exposes that opinion.

bash
# Which Salesforce CLI does Nimbus resolve, and is it supported?
nimbus toolchain sf status

# Include the resolved path and the CLI's own version line
nimbus toolchain sf status --verbose

# Machine-readable, for a CI gate
nimbus toolchain sf status --json

Resolution order is short and inspectable: NIMBUS_SF_PATH first, then sf on PATH. If a runner has two CLIs installed, --verbose tells you which one won.

To install or move the pin, the sf subcommands manage the official package directly. Ordinary Nimbus commands never modify your CLI — only these do.

bash
# Install an exact version
nimbus toolchain sf install --version 2.143.6

# See what updates exist, without installing one
nimbus toolchain sf update --check

# Move the pin deliberately
nimbus toolchain sf update --version 2.144.6

Assert the pin, do not just hope for it

Set nimbus.sf.version in nimbus.properties to assert an exact version. nimbus toolchain sf status then reports a mismatch, so CI can prove it reproduces the toolchain it was configured for rather than assuming it did.

properties
# nimbus.properties
nimbus.sf.version=2.143.6

Because the expected version now lives in the repository, it moves through code review like any other change, and every developer's machine is checked against the same number as the runner.

In a pipeline

yaml
# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: nimbus-solution/setup-nimbus@v1

      # One pinned version, held as a repository variable
      - run: nimbus toolchain sf install --version ${{ vars.SF_VERSION }}

      # Fail here, with a clear message, rather than three steps later
      - run: nimbus toolchain sf status

      - run: nimbus release validate --source-dir force-app --target-org staging

The ordering is the point. The toolchain check runs before anything expensive, so a drifted runner fails in seconds with an unambiguous message instead of failing forty minutes into a validation with a flag-parsing error.

Moving the pin

Pinning is not freezing. A pin nobody ever moves becomes its own liability — an eighteen-month-old CLI that no longer supports an API version you need. Treat the bump as a small, ordinary change:

  • Run nimbus toolchain sf update --check on a schedule to see what has shipped.
  • Bump nimbus.sf.version and the CI variable in one commit, so they cannot disagree.
  • Let the pipeline prove it on a sandbox target before it reaches production.

Next steps

The cheapest version of this is one command in your existing pipeline. Add nimbus toolchain sf status before your deploy step and you will find out immediately whether your runners agree with each other.