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:
# The line in almost every Salesforce pipeline
- run: npm install --global @salesforce/cliThat 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.
# Exact version, not "latest"
npm install --global @salesforce/cli@2.143.6
# What is actually resolved on this machine?
sf --versionThree ways to hold the number, in increasing order of durability:
- A CI variable. One
SF_VERSIONrepository or group variable, referenced by every workflow. Cheapest option, and enough for most teams. - A devDependency plus a lockfile. Add
@salesforce/clito the project'spackage.jsonand commit the lockfile. Now the version is versioned with the code, moves through review, and shows up ingit logwhen 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.
# 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 --jsonResolution 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.
# 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.6Assert 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.
# nimbus.properties
nimbus.sf.version=2.143.6Because 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
# .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 stagingThe 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 --checkon a schedule to see what has shipped. - Bump
nimbus.sf.versionand 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.