Pin one CLI version, authenticate CI with JWT, validate against production and quick deploy the validated set within its window, pass a test level on purpose in every environment, and watch long deployments with report and resume rather than a longer wait.
If you already installed Salesforce CLI but your pipeline still breaks, this playbook is your next step. It covers the post-install work that fails most often: authentication, deploy strategy, test levels, watching a deployment that outlived its wait, and CI troubleshooting. Every command was written against @salesforce/cli 2.152.14, which bundles @salesforce/plugin-deploy-retrieve 4.2.2, and checked against that version’s command reference rather than run in an org.
If you still need installation help, start with How to Install Salesforce CLI with NPM and keep How to Fix Salesforce CLI NPM Installation Errors open for common setup failures.
Why does a pipeline that passed everywhere else fail in production?
Because production is the one org where the defaults change under you. Three facts from the CLI command reference explain most release-night failures:
- The default test level depends on the target.
NoTestRunapplies only to development environments such as sandboxes, Developer Edition and trial orgs, and is the default there;RunLocalTestsis the default for production deployments that include Apex classes or triggers. A pipeline that never set a test level never ran your tests until production. - A validation is a dated artefact. Its job ID is valid for 10 days from when you started it, the validation must have succeeded, and
--use-most-recentonly reaches back 3 days. --waitdefaults to 33 minutes ondeploy start,validateandquick. When the CLI stops watching, the deployment carries on in the org without you.
Add authentication, which is a browser flow on your laptop and a certificate flow in CI, and you have the four places this playbook spends its time.
What does this playbook assume about your team, orgs and CI?
It assumes a small team, a sandbox path to production, and a Git-based pipeline. If your setup differs, the commands still hold; the trade-offs shift.
- Team size: two to ten developers sharing one or two repositories, with one person who owns the pipeline.
- Org strategy: scratch orgs or developer sandboxes for feature work, one integration sandbox, one UAT or staging sandbox, one production org. Scratch and sandbox orgs track source by default; production never does, which is why the conflict and “nothing to deploy” behaviours below appear only in the lower orgs.
- CI model: pull requests validate, merges to
mainpromote, approvals sit in the CI platform’s environment gates, and secrets live in the CI platform, never in the repo. - One CLI version, pinned in CI and documented for laptops.
These are my working assumptions from client projects, not platform rules.
What should I configure right after installing Salesforce CLI?
Set a known CLI version, confirm plugin health, and verify you can run core commands in the same shell your scripts use. This removes “works on my machine” drift before you touch authentication or deploys.
# @salesforce/cli 2.152.14
# 1) Verify version and executable location
sf --version
which sf
# 2) Check plugin state
sf plugins --core
# 3) Pin a known CLI version for reproducible CI
npm install @salesforce/cli@2.152.14 --global
Then store the chosen version in your CI config so local and pipeline behaviour stay aligned.
How do I set up Salesforce CLI auth for local development and CI/CD?
Use interactive browser auth for local work and JWT-based headless auth in CI. Mixing both in pipelines is a common source of unstable deploy jobs.
For local development, browser auth is simplest:
# @salesforce/cli 2.152.14
sf org login web --alias devhub --set-default-dev-hub
For CI, the Salesforce DX Developer Guide’s JWT flow needs three things in place before the CLI command will work:
A private key and self-signed certificate. The guide’s OpenSSL steps are four commands: generate an AES-encrypted key with
openssl genpkey, strip the passphrase withopenssl rsa, create a signing request withopenssl req -new, and sign it withopenssl x509. They produceserver.key, the private key you pass to the CLI, andserver.crt, the certificate you upload. The one-liner below is my shortcut to the same result, an unencrypted key and a self-signed certificate; in my pipelines an encrypted key fails the login:openssl req -x509 -sha256 -nodes \ -days 365 -newkey rsa:2048 \ -keyout server.key -out server.crtAn external client app or connected app in the target org. The guide now describes both. Following its connected app page: enable OAuth, select “Use digital signatures” and upload
server.crt. Grant the three scopes it lists: Manage user data via APIs (api), Manage user data via Web browsers (web), and Perform requests at any time (refresh_token,offline_access). Pre-authorise the CI user’s profile or permission set so no one has to click an approval.Secrets in your CI platform:
SF_CLIENT_ID(the app’s consumer key),SF_USERNAME, and the private key file content as encrypted secrets.
Then the CI auth command is:
# @salesforce/cli 2.152.14
sf org login jwt \
--client-id "$SF_CLIENT_ID" \
--jwt-key-file "$SF_JWT_KEY_PATH" \
--username "$SF_USERNAME" \
--instance-url https://login.salesforce.com
If JWT auth fails, check in this order: the key has no passphrase, the app’s scopes include api, the CI user is pre-authorised, and IP restrictions are not blocking your CI runner’s address range. The full setup, with the failure modes, is in Headless Auth: Salesforce CLI + GitHub Actions (No Browser Login). In CI, fail fast if auth does not succeed before deploy steps run.
How should I structure metadata deploy commands to reduce failures?
Deploy specific metadata paths intentionally, validate before pushing to production, and separate destructive steps from routine deploys. Smaller, explicit deploy scopes fail less and are easier to debug.
Use the validate-then-quick-deploy pattern for production releases. Validation runs your tests against the target org without committing changes. If validation passes, quick deploy pushes the same component set without re-running tests, cutting production deployment time.
# @salesforce/cli 2.152.14
# Step 1: Validate against production (runs tests, commits nothing)
sf project deploy validate \
--source-dir force-app \
--target-org "$SF_USERNAME" \
--test-level RunLocalTests \
--wait 60
# Step 2: Quick deploy the validated set (skips tests); --use-most-recent works on the
# machine that validated, within 3 days. On another machine pass --job-id instead.
sf project deploy quick \
--use-most-recent \
--target-org "$SF_USERNAME"
The constraints, from the reference for project deploy validate and project deploy quick: the job ID is valid for 10 days from when you started the validation, the validation must have succeeded, and --use-most-recent only finds job IDs validated in the past 3 days. When --use-most-recent finds nothing in that 3-day cache, the plugin stops with There are no recent job IDs available. When the job ID itself cannot be quick deployed, quick refuses it with this message:
Job ID can't be used for quick deployment. Possible reasons include the deployment hasn't been validated, has already been deployed, or the validation expired because you ran it more than 10 days ago.
Salesforce’s Trailhead unit on the org development model adds that the window holds as long as you do not perform another deployment or make major changes to the org, so re-validate if anything else has landed. The command-by-command decision is in sf project deploy start vs sf project deploy validate.
For non-production environments where you want a faster feedback loop, a direct deploy is fine:
# @salesforce/cli 2.152.14
sf project deploy start \
--source-dir force-app \
--target-org "$SF_USERNAME" \
--test-level RunSpecifiedTests \
--tests MyFeatureTest \
--wait 30
When you must run destructive changes, isolate them in their own pipeline job with explicit approval gates.
Which Apex test level should I use in each environment?
Use faster levels for lower-risk environments and stricter levels for production-bound changes, and pass the level explicitly so nobody inherits a default. The levels, from the reference’s --test-level description:
NoTestRun: scratch org setup and metadata-only experiments. Development environments only, and the default there.RunSpecifiedTests: feature branch validation with targeted test classes. The tests you name must give each class and trigger in the package at least 75% coverage individually, which the reference notes is different from the overall percentage.RunLocalTests: integration, UAT and production paths. Runs every test except those from installed managed and unlocked packages; the default for production deployments with Apex.RunAllTestsInOrg: only when policy or release criteria require managed package tests in the gate.
The production rule behind the defaults, from the Metadata API Developer Guide, is overall coverage of at least 75% with some coverage on every Apex trigger. Salesforce Help adds that the coverage restriction is not enforced for sandbox or Developer Edition orgs. Keep your test strategy written in-repo so engineers do not guess per pipeline.
How do I deploy to a sandbox without running tests?
Pass --test-level NoTestRun to sf project deploy start against the sandbox. The reference says NoTestRun applies only to development environments such as sandboxes, Developer Edition and trial orgs, and that it is the default there. A sandbox deploy with no test level therefore already skips tests. Saying it explicitly keeps the pipeline honest when the target changes.
# @salesforce/cli 2.152.14
sf project deploy start \
--source-dir force-app \
--target-org dev-sandbox \
--test-level NoTestRun \
--wait 20
When that is fine: developer sandboxes, metadata-only changes, and the first deploy of a feature branch where you want the compile errors before the test results. When it is not: the last sandbox before production. Run RunLocalTests there at least once, because production will not accept NoTestRun and will run the local tests by default, and the sandbox is your only rehearsal of that run. If your scripts still say sfdx force:source:deploy -l NoTestRun, they are calling a command that left sf (v2) in November 2024, as the version-drift section below explains; the replacement is the command above.
Where do I see pending and running Salesforce deployments?
In the org, Setup’s Deployment Status page; from the terminal, sf project deploy report. Salesforce Help says that to track deployments in progress or completed in the last 30 days, you enter Deployment in Setup’s Quick Find box and select Deployment Status. The page lists all deployments: change sets, Metadata API deployments including those started from the Salesforce Extensions for Visual Studio Code, and package installations. The page has separate In Progress, Pending Deployments, Failed and Succeeded sections; queued deployments sit under Pending, because one deployment runs at a time.
From the CLI, report checks or polls a deploy operation by job ID, or the most recent one started in the past 3 days:
# @salesforce/cli 2.152.14
# One check of the most recent operation from this machine
sf project deploy report --use-most-recent --target-org "$SF_USERNAME"
# Poll a specific job every second for up to 20 minutes
sf project deploy report --job-id "$SF_DEPLOY_JOB_ID" --target-org "$SF_USERNAME" --wait 20
The job ID itself is valid for 10 days, and on a machine other than the one that deployed you must pass --target-org, because the local cache that maps the job to the org is not there. sf project deploy resume takes over watching a deployment that timed out or was started with --async; the reference is clear that it does not restart the operation, which always continues once started. If a change set validation or deployment sits at In Progress far longer than it should, Salesforce’s knowledge article on that symptom names the most common cause as the deploying user lacking the Modify All Data permission.
Speed or safety: what are the trade-offs?
Every speed gain in a Salesforce pipeline is bought with a specific risk, and the table below names the price so you can pay it on purpose.
| Choice | Faster option | What it costs | Safer option | What it costs |
|---|---|---|---|---|
| Production release | deploy start with tests in one step |
The whole test run sits in the release window; a failure rolls back after you waited for it | validate then quick |
A perishable job ID (10 days, 3 for --use-most-recent), two pipeline stages |
| Test scope | RunSpecifiedTests on the changed classes |
75% per class and trigger individually; a missed test fails the deploy | RunLocalTests |
Runs every local test, including ones your change never touched |
| Where tests run | Local dry run in a sandbox with --dry-run |
Sandbox data and config differ from production | Validation in production | Production’s full suite and its timings |
| Feature orgs | Scratch orgs, source-tracked | Setup scripts to maintain | Developer sandboxes | Refresh cadence and shared state |
| CLI version | Always latest | Flag and default changes arrive unannounced into CI | Pinned and upgraded deliberately | Someone has to schedule the upgrade |
How do I speed up Salesforce CLI deploys without risky shortcuts?
Reduce deployment scope and avoid retesting unaffected metadata instead of lowering quality bars. The safest speed gains come from precision, not from disabling tests.
Focus on these levers:
- Deploy only changed package directories when your repo structure supports it.
- Use
sf project deploy previewto review what will be deployed before committing to a full run. - Prefer
RunSpecifiedTestsfor fast feedback in non-production stages. - Keep test data factories efficient so targeted tests finish quickly.
- Cache dependencies in CI to cut setup time before CLI commands run.
- Use
.forceignoreto exclude metadata that should never leave your repo. The Salesforce DX Developer Guide says the file excludes files from most project commands, includingproject deploy startandproject retrieve start, and follows.gitignorepatterns. A practical starting point:
# .forceignore
package.xml
# Admin-managed metadata
**/profiles/**
**/settings/**
# IDE and OS files
.eslintrc.json
.prettierrc
.gitignore
How do I prevent CLI and plugin version drift across the team?
Pin versions where stability matters and document upgrade cadence. Unmanaged updates can break commands, output parsing, and automation scripts without warning.
Team guardrails that work:
- Pin the CLI version in CI and publish it in
READMEor pipeline docs. - Avoid unreviewed plugin installs on shared runners.
- Schedule regular dependency updates and validate pipeline behaviour in a branch first.
- Track command changes in release notes before bumping major versions.
- If your scripts still use
sfdxcommands, migrate them to thesfequivalents. Salesforce announced on its CLI issue tracker that the deprecatedforce:source:*,force:mdapi:*,force:org:createandforce:org:deletecommands would leavesf(v2) on 6 November 2024, whenplugin-sourcestopped being bundled, so scripts that callsfdx force:source:deployor similar fail on current CLI versions unless that plugin is installed by hand. Salesforce’s Move from sfdx (v7) to sf (v2) page covers the switch itself: uninstall v7, install v2, and the commands you already run keep working after the move, which held until that November 2024 removal of the deprecatedforcefamilies. The page has no command table; the replacements are thesf projectandsf orgcommands the announcement points to.
How do I troubleshoot failed Salesforce CLI deploys quickly?
Capture the deploy ID, fetch detailed status, and classify errors into auth, metadata validation, test failure, or platform limits. Fast classification prevents wasted retries.
Use this debug flow:
# @salesforce/cli 2.152.14
# Start the deploy and capture the job ID from the JSON output
sf project deploy start --source-dir force-app --target-org "$SF_USERNAME" --wait 30 --json > deploy.json
# Pull the full report, with the component and test failures
sf project deploy report --use-most-recent --target-org "$SF_USERNAME"
In my pipelines the deploy output has occasionally ended without component-level detail; when a failure has no detail, report is the first move, and --junit with --results-dir keeps the test results as files.
What if the deploy succeeded but the CLI timed out?
The CLI prints The command has timed out, although the deployment is still running. Use "sf project deploy resume" to resume watching the deployment. and the deployment carries on in Salesforce. The CLI’s --wait is a client-side limit, separate from the server-side deploy.
When this happens:
- Run
sf project deploy resume --use-most-recentto watch it again, orreportfor a one-off status, or open Setup’s Deployment Status page. - If the deployment succeeded, your pipeline failed for nothing. Size
--waitfrom the measured run, or start with--asyncand poll withreportin a later step. - If the CLI hangs after otherwise successful operations, deleting the project’s
.sfdirectory has cleared it in my projects; treat that as a last resort, not a documented fix.
If your failures are install- or environment-related, use the fixes in How to Fix Salesforce CLI NPM Installation Errors.
What breaks at scale, with several repositories and orgs?
The commands stay the same; what breaks is ownership, ordering and the validation window. In my projects these are the recurring failures once a team passes one repository and one production org:
- Shared metadata with no owner. Profiles, permission sets, layouts and custom labels edited from two repositories overwrite each other on the way to the same org. Give each metadata type one owning repository, and
.forceignoreit everywhere else. - Ordering between repositories. A dependency deployed from repo B after repo A’s validation counts as another deployment to the org, which the Trailhead unit says invalidates the quick deploy window. Sequence the releases, or validate immediately before promoting.
- Per-org test suites.
RunLocalTestsruns every local test in the target org, so an org with another team’s slow tests sets the duration of your release. Measure each org’s suite and size--waitand--asyncper org, not globally. - One identity per org. An external client app or connected app, a certificate and a CI user per target org, each pre-authorised, so a certificate rotation in one org cannot break releases to another.
- Version pinning across repositories. Every repository pins the same CLI version and upgrades on the same day, or the first repository to upgrade becomes the one whose pipeline differs.
How do I set up a GitHub Actions pipeline for Salesforce CLI?
Define a workflow that installs the CLI, authenticates with JWT, validates against the target org, and then quick-deploys on success. This example covers the full path from push to production deploy.
Store your external client app or connected app consumer key, username, and private key as encrypted repository secrets (SF_CLIENT_ID, SF_USERNAME, SF_JWT_KEY).
# @salesforce/cli 2.152.14
name: Salesforce Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate:
name: Validate Metadata
runs-on: ubuntu-latest
outputs:
job_id: ${{ steps.validate.outputs.job_id }}
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Install Salesforce CLI
run: npm install @salesforce/cli@2.152.14 --global
- name: Write JWT key file
run: echo "${{ secrets.SF_JWT_KEY }}" > server.key
- name: Authenticate target org
run: |
sf org login jwt \
--client-id "${{ secrets.SF_CLIENT_ID }}" \
--jwt-key-file server.key \
--username "${{ secrets.SF_USERNAME }}" \
--instance-url https://login.salesforce.com
- name: Validate deployment
id: validate
run: |
sf project deploy validate \
--source-dir force-app \
--target-org "${{ secrets.SF_USERNAME }}" \
--test-level RunLocalTests \
--wait 60 \
--json > validate.json
echo "job_id=$(jq -r '.result.id' validate.json)" >> "$GITHUB_OUTPUT"
- name: Clean up key file
if: always()
run: rm -f server.key
deploy:
name: Quick Deploy to Production
needs: validate
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Install Salesforce CLI
run: npm install @salesforce/cli@2.152.14 --global
- name: Write JWT key file
run: echo "${{ secrets.SF_JWT_KEY }}" > server.key
- name: Authenticate target org
run: |
sf org login jwt \
--client-id "${{ secrets.SF_CLIENT_ID }}" \
--jwt-key-file server.key \
--username "${{ secrets.SF_USERNAME }}" \
--instance-url https://login.salesforce.com
- name: Quick deploy validated set
run: |
sf project deploy quick \
--job-id "${{ needs.validate.outputs.job_id }}" \
--target-org "${{ secrets.SF_USERNAME }}" \
--wait 60
- name: Clean up key file
if: always()
run: rm -f server.key
Pull requests run validation only. Pushes to main validate and then quick-deploy to production, using a GitHub environment with manual approval gates if you need them. The two jobs run on different runners, so the deploy job cannot use --use-most-recent: the quick command’s source resolves that flag from the deploy cache on the machine that validated, and a fresh runner has nothing to find. The validate job writes the job ID to a job output and the deploy job passes it with --job-id. In my pipelines the ID is read from .result.id in the --json output, as above, and the whole release still has to finish inside the validation’s 10 days.
Adapt this pattern for other CI platforms (GitLab CI, Bitbucket Pipelines, Azure DevOps) by translating the YAML structure and secret references to each platform’s format.
Should I use Salesforce CLI or DevOps Center for this pipeline?
Use Salesforce CLI when you need scriptable, repeatable automation in Git-based workflows. Use DevOps Center, which Salesforce’s Spring ‘23 release notes list as generally available, when your team prefers guided releases with less terminal-heavy operation.
A simple decision rule:
- Choose Salesforce CLI for high automation, custom checks, and advanced CI control.
- Choose DevOps Center for admin-heavy teams and guided change promotion with a built-in UI.
- Use both when needed: DevOps Center for release orchestration, CLI for custom validations and edge-case deploy logic.
What should I check first when a Salesforce CLI pipeline fails?
Run this list before you change anything:
sf --versionon the runner against the pinned version. Match them.- Did authentication succeed, and as which user against which instance URL?
- Which org did the deploy target, and is it production or a sandbox?
- Which test level ran, including the default the org gave you?
- The job ID, its start time, and whether it is a validation, a deployment or a quick deploy.
sf project deploy report --job-id <id>for the org’s view of the result before you trust the runner’s exit code.- Has anything else been deployed to the org since the validation you are about to quick deploy?
- Is the runner’s own timeout shorter than
--wait?
Related guides you can use now
If you need help before or during these CI/CD workflows, start with these:
- sf project deploy start vs sf project deploy validate: When to Use Each
- Headless Auth: Salesforce CLI + GitHub Actions (No Browser Login)
- How to Install Salesforce CLI with NPM
- How to Fix Salesforce CLI NPM Installation Errors
References
- Salesforce CLI Command Reference, project deploy start (test levels and their defaults,
--wait), project deploy validate and project deploy quick (the 10-day job ID,--use-most-recentand its 3-day reach), project deploy report and project deploy resume, and org login jwt. - The plugin message sources behind that reference text: deploy.metadata.md (
--test-leveldescription,error.ClientTimeout), deploy.metadata.quick.md (theCannotQuickDeploymessage), deploy.metadata.report.md and deploy.metadata.resume.md. - Salesforce DX Developer Guide, Authorize an Org Using the JWT Flow, Create a Private Key and Self-Signed Digital Certificate, Create a Connected App in Your Org and How to Exclude Source When Syncing.
- Metadata API Developer Guide, deployRecentValidation() (overall 75% and trigger coverage for production) and Running a Subset of Tests in a Deployment (the per-class rule).
- Salesforce Help, Deploy a Change Set (coverage not enforced for sandbox or Developer Edition orgs), Monitor Deployments (the Deployment Status page), the knowledge article Salesforce Change Set Validation or Deployment Stuck in ‘In Progress’, and the Spring ‘23 release note DevOps Center (Generally Available).
- Trailhead, Org Development Model, Test and Deploy Changes: the quick deploy window holds as long as there is no other deployment or major change to the org.
- Salesforce CLI issue tracker, #2974: the November 2024 removal of the deprecated
forcecommand families; Salesforce CLI Setup Guide, Move from sfdx (v7) to sf (v2). - @salesforce/cli on npm: version 2.152.14 and its
@salesforce/plugin-deploy-retrieve4.2.2 dependency.
Need a Salesforce release pipeline that holds up?
I take short specialist contracts to harden Salesforce CLI pipelines, cut release risk and stop repeat deployment failures.