How-to

Quick deploy in Salesforce

A production deploy that runs the full Apex suite can take hours, and it takes them inside your release window with the org half-changed. Quick deploy moves that cost out of the window: validate first, run the tests then, and when the window opens ship the job Salesforce already checked.

Short version

sf project deploy validate checks a payload against the org and runs the Apex tests, then hands back a job ID without saving anything. sf project deploy quick --job-id deploys that job. Nothing is retransmitted and no test re-runs. The validated job stays quick-deployable for 10 days.

What quick deploy actually is

A normal deploy does two things at once: it transmits your metadata and it runs the Apex tests the target org requires. In production, the second part is the long pole — a RunLocalTests validation on a large org routinely takes 45 minutes and can take considerably longer. If that happens during your release window, the window is mostly test execution.

Salesforce lets you split the two. A validation uploads the payload, checks deployability against the org's real metadata, runs the Apex tests, and then throws away the result — nothing is saved. What you keep is a job ID. Later, a quick deploy points at that job ID and commits it. Because the org already has the payload and already has the test results, the quick deploy retransmits nothing and re-runs nothing. Minutes, not hours.

The rules worth knowing before you build a pipeline on it:

  • The validation must have succeeded. A failed validation produces no reusable job.
  • Tests are mandatory. sf project deploy validate requires a test level — RunSpecifiedTests, RunLocalTests, RunAllTestsInOrg, or RunRelevantTests. There is no NoTestRun validation, because the test results are the thing being banked.
  • 10 days. After that the job is no longer quick-deployable and you validate again.
  • Same org. A job validated against staging cannot be quick-deployed to production. Each org needs its own validation.
  • --dry-run is not the same thing. sf project deploy start --dry-run validates, but yields nothing reusable. It is the right check for a sandbox; it is not the command that sets up a quick deploy.

The two commands

bash
# 1. Validate. Runs the Apex tests, saves nothing, returns a job ID.
sf project deploy validate \
  --source-dir force-app \
  --target-org production \
  --test-level RunLocalTests

# 2. Later — deploy that exact job. No retransmit, no test re-run.
sf project deploy quick --job-id 0Af... --target-org production

# Or, from the same machine, without copying the ID around
sf project deploy quick --use-most-recent --target-org production

Between the two is wherever your approval lives: a change record, a release call, a manual gate in the pipeline. That gap is the entire point of the pattern — the expensive work happens before the approval, and the approval is followed by a short, predictable deploy.

Where the pattern gets fragile

The job ID is the only thing connecting the validation to the deploy, and a bare ID carries no evidence. It does not tell you which commit was validated, which tests passed, whether the source has moved since, or whether the org's metadata has drifted underneath it. Two days later, "deploy 0Af0X…" is a string someone pasted into a terminal, and the honest answer to "what is in it?" is that you would have to go and look.

That is fine when one person validates and deploys within the hour. It stops being fine when validation runs on a CI machine, approval happens in a ticket, and the deploy runs from somewhere else entirely.

The Nimbus way

Nimbus wraps the same platform mechanism in a receipt — a JSON record written by nimbus release validate that carries the exact bundle digest, the git revision, the target-org fingerprint, the local gate results, the Salesforce validation job ID, and its quick-deploy expiry. The receipt is what you approve, and the receipt is what you deploy.

bash
# Validate: local gates, then Salesforce validates the same bytes,
# then a receipt is written.
nimbus release validate \
  --source-dir force-app \
  --target-org production \
  --min-coverage 80

# Inspect what you are about to approve
nimbus release list
nimbus release get <releaseID>

# After approval — quick-deploys the validated job
nimbus release deploy <releaseID> --target-org production --confirm-production

Two things happen in validate that a bare sf project deploy validate does not do. First, the payload is snapshotted — the bundle is stored, so what gets deployed later is the bytes that were checked, not a re-read of a worktree that may have moved. Second, the local gates run against that snapshot: your Apex tests locally, a coverage floor via --min-coverage, and any configured quality thresholds. Local tests take seconds, which is why the org-side test level for a sandbox target defaults to none — the same tests already ran.

nimbus release deploy then re-checks the receipt before it does anything: same org, same bytes, unexpired validation, compatible toolchain. On any mismatch it refuses. There is deliberately no --force.

Which path the deploy takes

Nimbus tells you which of the two mechanisms is running, because the wait is very different:

  • Quick deploy. For production-style targets, Nimbus validated with sf project deploy validate, so there is a reusable job. The deploy references it and retransmits nothing.
  • Bundle resend. For sandbox and developer targets the platform-correct check is a dry run, which yields nothing reusable — and the receipt records that honestly. The deploy then ships the stored bundle, so the validated bytes are still the deployed bytes.

nimbus release get prints the remaining quick-deploy window on a receipt that has one, so you know before the release call whether you are inside the 10 days or need to revalidate.

Validate on one machine, deploy from another

Receipts and bundles are portable CI artifacts. The natural pipeline shape is: validate on push, upload the receipt and bundle, gate on a manual approval, then deploy on a different runner using the artifact rather than a fresh checkout.

yaml
# .github/workflows/release.yml (shape, not a drop-in)
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: nimbus-solution/setup-nimbus@v1
      - run: nimbus release validate --source-dir force-app --target-org production --json
      - uses: actions/upload-artifact@v4
        with: { name: release, path: .nimbus/release }

  deploy:
    needs: validate
    environment: production   # the approval gate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with: { name: release, path: .nimbus/release }
      - uses: nimbus-solution/setup-nimbus@v1
      - run: nimbus release deploy --receipt <releaseID> --target-org production --confirm-production

If the deploy submits but the outcome is never confirmed, Nimbus exits 26 and names the job to watch. If the org's baseline components changed after validation, it exits 27 for drift rather than quietly overwriting someone else's work. And if the deploy sticks in Pending, see fixing a stuck deployment.

The release receipt workflow is a Pro feature. nimbus deploy — validate and deploy in one command, without the receipt lifecycle — is the free, safe default.

Next steps

Quick deploy is worth setting up the moment your production test run outgrows your release window. Attaching evidence to the validated job is worth it the moment the person deploying is not the person who validated.