Docs/Ship

Ship

Gate, validate, release, and report on the same payload from local development through CI.

Commands

nimbus sf

Run any Salesforce CLI core command or installed sf plugin through Nimbus. Arguments, the working directory, terminal input/output, interactive prompts, and the Salesforce process exit code pass through unchanged. Nimbus notices are suppressed so JSON output remains machine-readable.

bash
nimbus sf org login web
nimbus sf data query --query "SELECT Id FROM Account" --target-org dev
nimbus sf project deploy start --source-dir force-app --target-org dev
nimbus sf package version create --package core --code-coverage

Flags after nimbus sf belong to Salesforce CLI; Nimbus does not parse them. The Salesforce CLI plugin intentionally does not expose ansf nimbus sf command because the user is already insidesf. Recursive nimbus sf nimbus ... invocations are rejected.

nimbus toolchain sf

Report which Salesforce CLI Nimbus resolves for remote work, and whether that version is supported. Installation uses Salesforce's official npm package; updates delegate to the resolved Salesforce CLI. Ordinary Nimbus commands never install or replace Salesforce CLI implicitly.

bash
nimbus toolchain sf status
nimbus toolchain sf status --verbose   # resolved path, reported version line, policy
nimbus toolchain sf status --json      # stable shape for CI and editors
nimbus toolchain sf install            # official @salesforce/cli stable package
nimbus toolchain sf install --version 2.142.7
nimbus toolchain sf update --check     # read-only available-version check
nimbus toolchain sf update             # delegate to sf update stable

Resolution order

FlagDefaultDescription
NIMBUS_SF_PATH-Explicit override. Pointing it at a missing or non-executable file is an error rather than a silent fallback to PATH
sf on PATH-Used when no override is set

Flags

FlagDefaultDescription
--jsonfalseEmit the status as JSON on stdout
--verbosefalseInclude the resolved path, the CLI's own version line, and the supported/tested versions

Pinning a version

Set nimbus.sf.version in nimbus.properties to assert an exact Salesforce CLI version. Status reports a mismatch so CI can prove it reproduces the configured toolchain.

bash
# nimbus.properties
nimbus.sf.version=2.142.7

Install that exact version through Nimbus's assisted official-package flow:

bash
nimbus toolchain sf install --version 2.142.7

Install and update flags

FlagDefaultDescription
install --version VERSION-Install an exact semantic version from the official @salesforce/cli npm package
install --channel stablestableInstall the current stable official package
update --checkfalseAsk Salesforce CLI to list available versions without installing
update --version VERSION-Delegate installation of an exact update to the resolved Salesforce CLI
update --channel CHANNELstableDelegate an update for the selected Salesforce CLI channel

Exit codes

The command exits non-zero when the toolchain is not usable as configured, so a CI bootstrap step can gate on it: Salesforce CLI missing, a legacy sfdxgeneration, or a violated nimbus.sf.version pin. A version that merely trails the tested one is reported but exits zero.

Nimbus's local loop — test, validate, exec,lsp, daemon — does not need Salesforce CLI at all, andnimbus doctor reports a missing CLI as a warning rather than a failure for that reason.

nimbus deploy

The safe one-shot Salesforce deployment. Nimbus snapshots the selected payload, runs local semantic validation and Apex tests against that snapshot, validates the same bundle in Salesforce, and only then deploys it. This gated path is available on Free; a raw bypass remains explicit as nimbus sf project deploy start ....

bash
nimbus deploy --target-org staging --source-dir force-app
nimbus deploy --target-org production --manifest manifest/package.xml --confirm-production
nimbus deploy --target-org qa --metadata ApexClass:AccountService --metadata ApexClass:AccountServiceTest

Production targets require an interactive typed confirmation or the explicit --confirm-production flag. Nimbus records the org as a one-way fingerprint and never puts the alias, username, access token, or instance URL in the receipt. Generated receipts and bundles live under .nimbus/releases/.

Selection and gate flags

FlagDefaultDescription
--target-org-Salesforce org alias or username
--release-profile-Named nimbus.release profile to source the target org and gate settings from (Pro)
--source-dir-Source directory; repeatable and mutually exclusive with the other selector kinds
--manifest-Package manifest path
--metadata-Metadata component selector; repeatable
--metadata-dir-Metadata API directory or zip
--nimbus-tests*Nimbus local test pattern
--min-coverage0Minimum local line coverage percentage
--test-levelorg-dependentSalesforce deployment test level. Unset resolves to RunLocalTests for production and NoTestRun elsewhere
--tests-Salesforce tests for RunSpecifiedTests; repeatable
--api-versionproject versionSalesforce API version
--pre-destructive-changes-Pre-deploy destructive manifest
--post-destructive-changes-Post-deploy destructive manifest
--allow-dirtyfalseAllow and record an uncommitted Git worktree
--confirm-productionfalseExplicit production approval for CI/non-interactive use
--report-mismatchfalseOpt in, for this run, to submit a sanitized report if Salesforce rejects a payload your local gates passed
--no-report-mismatchfalseNever submit a mismatch report for this run; overrides configuration and --report-mismatch
--jsonfalseEmit one stable JSON envelope

nimbus release Pro

Feature deep-dive: release management.

Split validation from approval and deployment with a portable, immutable receipt. The exact uncompressed bundle is content-addressed; fallback deployment verifies and deploys that stored artifact, while production validation retains Salesforce's org-side job for quick deploy. Missing, changed, expired, wrong-org, or incompatible receipts are rejected with no override flag.

bash
nimbus release plan --target-org staging       # read-only: what would reconciling the org to source change?
nimbus release validate --target-org staging --changed   # bundle and validate only source − org
nimbus release validate --release-profile production
nimbus release deploy --receipt .nimbus/releases/rel_....json --release-profile production --confirm-production
nimbus release run --release-profile staging
nimbus release status                    # org deploy queue + stuck-Pending diagnosis
nimbus release watch 0Af... | rel_...    # live org-side progress until terminal
nimbus release promote rel_... --target-org prod   # same bundle, validated against the next org
nimbus release rollback rel_... --target-org prod --confirm-production   # restore what that deploy overwrote
nimbus release requeue rel_...          # cancel a stuck Pending job, resubmit the validated bundle
nimbus release keygen                   # create a signing key + trust it in this project
nimbus release verify rel_...           # offline: re-hash the bundle, check the signature
nimbus release list
nimbus release get rel_.... --json

With no selector, validate and run default their source to the packageDirectories in sfdx-project.json, the same way sf project deploy start does — so a bare nimbus release validate --target-org <org> just works. An explicit --source-dir, --manifest, or --metadata always wins. After a successful validate, the printed Next: line is copy-paste runnable: it carries the --target-org (or --release-profile) the deploy needs, since the receipt stores only a one-way org fingerprint, never the alias.

Exit codes: 0 success, 20 configuration, 21local gate, 22 Salesforce validation, 23 deploy,24 production confirmation, 25 release integrity violation (tampered / mismatched / missing bundle), 26 deploy submitted but outcome unconfirmed (run nimbus release watch <job>),27 drift — baseline components changed in the org after validation (revalidate, or deploy again with --override-drift),28 plan --fail-on-diff found source↔org differences (for scheduled reconciliation jobs), 29 counter-signature required but missing, or a production deploy by the same identity that validated (segregation).

Receipt and bundle files may be uploaded between CI jobs. When a downloaded receipt is passed by path, Nimbus locks, reads, and updates it beside its sibling bundle instead of silently creating a different local copy.

The deploy job still needs the project skeleton on disk. A receipt and its bundle are not enough on their own: sf deploys in project context, so the checkout that runs nimbus release deploy must containsfdx-project.json and every path its packageDirectoriesdeclares (an empty force-app/ is fine, but the directory must exist). Job-status polling (report / resume / cancel) is decoupled from local project state — Nimbus runs those org-side queries from a neutral throwaway project — so a broken checkout no longer makes a poll die while the org deployment succeeds. But the deploy command itself is still a project-context operation, so check the project skeleton out in the deploy job.

Named profile

bash
# nimbus.properties
nimbus.release.profile=staging
nimbus.release.staging.targetOrg=staging
nimbus.release.staging.source.dirs=force-app
nimbus.release.staging.nimbusTests=*
nimbus.release.staging.minCoverage=75
nimbus.release.staging.minMutationScore=80
nimbus.release.staging.codeAnalyzerThreshold=2
nimbus.release.staging.requireCodeAnalyzer=true
nimbus.release.staging.validateLocal=true
nimbus.release.staging.validateSalesforce=true
nimbus.release.staging.requireCleanGit=true
nimbus.release.staging.mismatchReporting=ask
nimbus.release.staging.permissionSeamPolicy=allow
nimbus.release.staging.sfVersionPolicy=compatible
nimbus.release.staging.requireSigned=false
nimbus.release.production.requireCountersigned=true

codeAnalyzerThreshold (1-5) is optional. When set, the local gate runs Salesforce Code Analyzer (sf code-analyzer) against the staged bundle and fails the release if any violation is at that severity or more severe (1=Critical … 5=Info; e.g. 2 blocks Critical and High). The full violation counts by severity are recorded on the signed receipt whether or not the gate blocks — so the receipt carries static-analysis (form) evidence next to the behavioral (tests, coverage, mutation) evidence, both tamper-evident. It reuses the code-analyzer already in your Salesforce CLI; no separate scanner to install. The receipt also records the ruleSelectorand a policyDigest — a fingerprint over the rule selection, engine versions, and any committed analyzer config — in the signed core, so whichrules produced the counts is itself answerable and tamper-evident.

requireCodeAnalyzer=true makes the gate fail closed — the complement to requireSigned/requireCountersigned. Without it the gate is opt-in and its absence is silent: a receipt with no analysis looks identical to a clean scan. With it, a profile refuses to validate or deploy a receipt that carries no static-analysis result at all (it requires codeAnalyzerThreshold to be set). The setting for a regulated profile that must never ship un-scanned code.

minMutationScore (0-100) is optional. When set, the local gate runs mutation testing against the staged bundle — using nimbusTests as the test selection — after the tests pass, and fails the release if the measured mutation score is below the threshold. It is skipped entirely when unset, since mutation testing is expensive; set it only on profiles where you want tests held to a proven quality bar, not just a coverage percentage.

maxReceiptAge (a duration such as 240h) caps how old a receipt may be at deploy time. It applies to every receipt, not only quick-deployable ones — a validated bundle that has aged past the window deploys against a drifted org, so nimbus release deploy refuses it and asks you to revalidate. It defaults to the 10-day Salesforce quick-deploy window. Independently,nimbus.release.keepReceipts (default 30) caps how many receipts a successful validate retains under .nimbus/releases: the oldest terminal receipts (failed / succeeded / cancelled) beyond the cap are pruned along with their bundles, while still-deployable (org_validated /deploying) receipts and the one just created are always kept.

nimbus release deploy consumes an already-validated receipt, so it honors only --receipt (or the positional receipt), --target-org,--release-profile, --confirm-production, and--json. It reruns no gates and restages no source, so the validate-time selectors and thresholds are not offered there. Passing a receipt both positionally and via --receipt is an error rather than a silent pick.

permissionSeamPolicy is allow (default),block-custom, or block-all. Nimbus runs tests as a sysadmin-equivalent mock user. The two seam classes resolve differently, and conflating them is a common misread:

  • FLS / object permissions default to true (allow) outside System.runAs() — a field or object check passes in Nimbus even when the org's running user would be denied. This is the permissive seam.
  • Custom permissions ($Permission.X /FeatureManagement.checkPermission) do not default to allow. They resolve against seeded permission-set state and default to deny unless the permission is granted to the mock user. So an ungranted custom permission already reads false, exactly as it would in the org — no seam, nothing to block. A block-custom seam fires when agranted custom permission is read through the default path outsiderunAs: the check passed because the seed granted it, not because the real running user holds it. The gate is deliberately narrow — it is not “unfireable”, it fires precisely on the granted-and-unguarded case.

block-custom fails the release on custom-permission seams (the highest-risk case, because those reads look like business logic, not security) while still allowing the FLS / object-perm seams. block-all blocks on any seam.allow never blocks, but the seam counts are always recorded on the receipt, so the divergence stays visible even under the permissive default.

Deploy queue visibility

Salesforce runs one deployment at a time per org and hides the queue, so a deploy waiting its turn is indistinguishable from a hung one — and a Pending job the scheduler silently dropped is indistinguishable from one about to start.nimbus release status reads the org's deployment queue, shows each job's position, live progress counters, and recent history correlated with your local receipts, and calls out a job that is genuinely stuck — Pending beyond a threshold, never started, with nothing running that explains the wait — together with the exact remedy. Jobs Nimbus did not create are diagnosed but never touched. The threshold is configurable via nimbus.release.stuckPendingAfter. Status is read-only and not Pro-gated: knowing your deploy is stuck is safety.

nimbus release status --json emits a stable, versioned document (schemaVersion, lowerCamelCase fields, whole-second durations) built for scripted gates. Active and in-flight jobs are always shown in full; the terminal recent-history list is capped — default 20, overridable with --limit — so a busy org's full day of deploys never floods a consumer.

Self-healing releases

Release validation and deployment submit asynchronously and own the wait: Nimbus polls the org, streams component and test counters as progress, and — when its own job sits Pending beyond a threshold with nothing running that explains the wait — cancels and resubmits the identical validated bundle automatically, with bounded retries. Nothing has executed org-side for a job that never started, and the payload cannot drift because it is the stored bundle, so no re-confirmation is asked; every recovery attempt is recorded on the receipt. Deployments Nimbus did not create are never touched. nimbus release watch follows any job or receipt read-only and mirrors the outcome in its exit code.

When a job stuck while nothing was attending it, nimbus release requeueapplies the same remedy by hand: it takes a receipt — never a bare job ID — verifies the org matches the receipt's fingerprint and the job is genuinely Pending and never started, then cancels it and resubmits the stored, digest-verified bundle. Foreign jobs are diagnosed by nimbus release status but never touched, so requeue can only ever act on a deployment Nimbus itself validated.

FlagDefaultDescription
nimbus.release.pollInterval5sHow often a running job is polled
nimbus.release.autoRequeueAfter5mPending age before an attended job is cancelled and resubmitted
nimbus.release.maxRequeues2Self-recovery attempts before the stall is surfaced as an error
nimbus.release.stuckPendingAfter3mPending age before release status flags a job as likely stuck

Plan — read-only source ↔ org reconcile

nimbus release plan answers “what would deploying my source actually change?” before any gate runs: it retrieves the org's current copy of the metadata types present in your selection into a throwaway directory, compares normalized content, and reports each component as + new in source,~ changed, unchanged, or - org-only (what a destructive change would remove — plan only ever reports it). Nothing deploys, no receipt is written, and the project's own source is never touched. Line-ending and trailing whitespace differences are not changes.

Human output caps each category at 20 rows so a shared org's thousands of foreign components cannot drown your deployable changes; --json always carries the complete component list plus counts and the compared metadata types (component states use the same camelCase as the count keys: orgOnly,notCompared). Source components whose type plan cannot retrieve for diffing (profiles, for example) are reported as not compared rather than guessed at. Exit is 0 whether or not differences exist — unless--fail-on-diff is set, which exits 28 on any divergence so a scheduled CI job can alert when the org has drifted from source (the compensating control for deploys that bypass Nimbus entirely).

Promote — the same bundle through your environments

nimbus release promote <receipt> --target-org prod carries a validated (or already deployed) release's exact bundle to another org and validates it there, writing a new receipt chained to its origin via promotedFrom. Promote never re-collects from the worktree — the worktree may have moved on; the stored bundle has not — so the bundle digest and source fingerprint are byte-identical across the whole chain. That is the provable claim “what you validated in staging is what reached production”, and it sits under the receipt signature.

What re-runs and what doesn't follows one rule: environment-specific work re-runs, byte-determined work is inherited. Nimbus's local execution is hermetic, so local gate results are a pure function of the bundle bytes — identical bytes carry their proof, recorded as an inheritance note in the transition trail. The Salesforce validation always re-runs against the destination, because every org drifts on its own schedule; a changed-set baseline is likewise recomputed against the destination so the deploy-time drift check guards the org actually being changed. Promote validates only — deploy the receipt it prints, with --confirm-production where required.

Declare your promotion chain once and --next walks it:

bash
# nimbus.properties
nimbus.release.environments = dev, uat, prod

nimbus release promote rel_... --next     # from the receipt's org to the next stage

The receipt stores only a one-way org fingerprint, so --next derives the receipt's position by fingerprinting each configured alias. Promoting past a configured stage (dev straight to prod) is allowed but never silent: the receipt records the number of skipped stages inside the signed core, release get and release list show it, and hiding the jump would break the signature.

Changed-set releases and drift

--changed (on validate, run, anddeploy's free sibling) reduces the payload to source − org: only components that are new in source or differ from the target org are bundled and deployed, plus their -meta.xml companions and the project scaffolding a DX deploy requires. The local gates still stage and run the full suite — a smaller payload never means less proof. On a large org this turns an hours-long full validation into minutes: validate 3 changed classes, not 3,000 unchanged ones. Config equivalent: nimbus.release.<profile>.changedOnly=true. If source already matches the org, validate refuses with “nothing to release” rather than writing an empty receipt.

A changed-set receipt records a baseline: the org-side content digest of every component it will overwrite, captured at validation and covered by the receipt signature. At deploy time Nimbus re-retrieves exactly those components (member-precise, not whole types) and compares. If the org moved underneath the receipt — someone hotfixed a class directly in production after you validated — the deploy is refused with exit 27 and the drifted components are named. There is no silent overwrite: --override-drift lets source win for this one deploy, and the override is recorded on the receipt (outside the signed core, so the validation-time signature stays intact) and in the transition trail. Overriding is deliberately flag-only — it cannot be set in configuration.

Rollback — restore what a deploy overwrote

Just before every deploy, Nimbus captures the org's current content of the payload's components as a snapshot bundle beside the receipt (a changed-set deploy reuses the drift check's retrieve, so it costs no extra org round-trip; capture is best-effort and never blocks the deploy — a skipped capture is recorded in the transition trail). nimbus release rollback <receipt> sends that snapshot back through the assured pipeline — check-only Salesforce validation of the snapshot bytes, then deployment — as a new receipt chained via rollbackOf, in one step, with --confirm-production required on production.

Two limits are stated rather than papered over. Components the rolled-back deploy added are not deleted — the snapshot holds prior content, and a component that did not exist has none; removal is a destructive change and stays a separate, explicit step. And local gates do not run — the payload is the org's own previous state, not developer source, so the org-side validation is its oracle, exactly as when that state first shipped. Both appear in the receipt's transition trail.

Rollback carries its own drift guard: before restoring, Nimbus compares the org's current content of the snapshot's components against what the rolled-back deploy shipped. If the org moved after that deploy — a post-incident hotfix, say — restoring the snapshot would erase that change too, so the rollback refuses (exit 27, components named) unless --override-drift is given; the override is recorded on the receipt with the actor who granted it.

Signed receipts

A receipt states what was validated; a signed receipt also proves who validated it. nimbus release keygen creates an Ed25519 key: the private half lives in ~/.nimbus/release-signing.json (user-only permissions, never inside the project), and the public half is appended to .nimbus/release-signers.json in the project root. Commit that roster — adding or removing a release signer then goes through code review like any other change. If you gitignore .nimbus/ (for the db and receipts), commit the roster with git add -f .nimbus/release-signers.json; once tracked, git follows it normally. An uncommitted roster silently breaks verification for everyone else, so keygen prints the right command for your repo.

Once a key exists, every release validate signs the receipt's immutable core (digests, gates, validation result, org fingerprint, toolchain) automatically, and release deploy verifies signed receipts against the committed roster before anything is sent: a receipt whose signed core was modified after signing, or whose signer is not on the roster, is refused as an integrity violation (exit 25). Verification is fully offline — the receipt embeds the public key, the roster decides trust.

Signing is opt-in per project. Unsigned receipts keep working until a profile sets requireSigned=true, which refuses to deploy any unsigned receipt — the natural setting for a production profile once the team has keys.

Who did what: the signature answers who validated; deploy-time events carry their own actor. When a signing identity exists (a key, or NIMBUS_SIGNING_IDENTITY in CI), the transition trail records who started the deploy, who granted a drift override (driftOverride.by), and who ran a rollback. An optional --change-ref JIRA-1234 on validate/run/rollback links the receipt to the process that authorized it — inside the signed core, so stripping the ticket reference breaks the signature; promote inherits it with the bundle. Receipts still never carry org aliases, but the machine that ran a release keeps a local, non-portable sidecar (.nimbus/org-aliases.json) so release get can say staging instead of a fingerprint — labeled as a local alias, and absent on any other machine.

Key lifecycle: nimbus release keygen --revoke <keyId>marks a key untrusted (lost laptop, leaked CI seed, departed teammate). Revocation is conservative — verification refuses every receipt signed with a revoked key, whenever it was signed, because an unsigned timestamp cannot prove a signature predates the compromise. The entry stays in the roster: the revocation is itself part of the record. Rotate the CI secret, keygen the replacement, revalidate what must stay deployable.

Trust is visible where humans read the evidence, not only at deploy. nimbus release verify <receipt> re-hashes the bundle against the recorded digest and checks the signature against the roster, fully offline — a tampered bundle, a forged field, or an untrusted signer fails with exit25; an unsigned receipt passes with an explicit warning. release get runs the same checks and printsIntegrity: and Signature: lines on every receipt — an unsigned receipt is labeled UNSIGNED — this view is unverified evidence, so an evidence reviewer is never fooled by numbers the file merely claims. Every receipt also records expiresAt (10 days by default,maxReceiptAge to change), so an archived artifact answers “validate Friday, deploy Monday?” on its own.

The displayed signer comes from the roster, not the receipt's own signature.signer string — that string is unsigned, so verification resolves the name from the committed keyId→name binding and flags any mismatch. For gates that must fail closed, release verify --require-signed exits25 on an unsigned receipt instead of passing with a warning.

Two attestations: validation and deploy. The validation signature covers the facts fixed at validation time — gates, digests, change ref, target org, toolchain, baseline. A separate deploy counter-signature covers the terminal deploy outcome — who deployed, the final status, any drift override — chained to the receipt so it can't be lifted onto another release. release get and verify show both:Deploy: counter-signed by <name> when attested, ornot counter-signed (status and actor are recorded, not attested) otherwise. This closes the gap where a signed receipt's deploy status and actor could be appended without breaking the signature.

Counter-signing is per-profile: nimbus.release.production.requireCountersigned=truedemands it. When required, the deploy must carry a signing identity (a dev's key or CI's NIMBUS_SIGNING_KEY), and on a production target that identity must differ from the validator — segregation of duties, so authorization is distinct from execution. A production deploy by the same identity that validated is refused with exit 29; a second person must deploy.

bash
nimbus release keygen                          # key + roster entry for git config user.email
nimbus release keygen --signer you@team.dev    # explicit identity
nimbus release keygen --ci                     # also print the seed once, for a CI secret

# headless CI signs via environment
NIMBUS_SIGNING_KEY=<seed> NIMBUS_SIGNING_IDENTITY=release-bot nimbus release validate ...

# refuse unsigned receipts on production
nimbus.release.production.requireSigned=true

Salesforce test level

Leave salesforceTestLevel unset and Nimbus resolves it from the target org: RunLocalTests for production, which Salesforce requires, andNoTestRun everywhere else. Nimbus has already run the whole suite locally in seconds by that point, so having a sandbox re-run every test in the org pays twice for the same answer and is the most expensive thing a release does. The org stage on a sandbox is there to prove deployability against real org metadata, which needs no tests.

Set it explicitly to override. On Winter '26 and later,RunRelevantTests runs only the tests relevant to the deployed metadata — Salesforce measures it at roughly 93% faster thanRunLocalTests — and is the better production choice where the org supports it.

bash
nimbus.release.production.salesforceTestLevel=RunRelevantTests

A validation that runs no tests cannot be quick-deployed, so Nimbus records no quick-deploy window for it and a later deploy re-sends the verified bundle.

Select a named profile with --release-profile. This is deliberately distinct from the global --profile, which activates a Quarkus-style%profile. configuration overlay.

Mismatch reporting

When Salesforce rejects the exact payload your local gates passed, and Nimbus classifies the rejection conservatively as a likely Nimbus compatibility gap — every reported problem looks like a compiler diagnostic Nimbus claims to cover, and no org- or project-dependency pattern matched — Nimbus can send a small, sanitized report so that gap becomes a permanent regression case. Org and project mismatches (missing dependency, managed package, coverage policy, org-only field) are never treated as Nimbus defects, and the deploy stays failed regardless of reporting.

A report contains only: the Nimbus and Salesforce CLI versions, the API version, the org kind (production/sandbox/…), the Salesforce test level, the classification, a dedupe fingerprint, the local-gate summary (validate/tests passed, tests run, coverage), and the rejection's error lines after they pass through the anonymizer. It never contains source code, org IDs, aliases or usernames, file paths, instance URLs, or record data — custom identifiers and paths are renamed while line numbers and standard platform vocabulary (Method does not exist, ApexClass) survive so the report stays diagnosable.

Reporting is opt-in. The mode is nimbus.release.<profile>.mismatchReporting=never|ask|always; ask is the default and only prompts in an interactive terminal — in CI, and inside a --json contract, ask behaves as never. --report-mismatch opts in for a single run and --no-report-mismatch always wins. When prompted, Nimbus prints the full sanitized record and the count of renamed identifiers before asking, defaulting to No. Every eligible record is always saved locally under .nimbus/releases/<releaseId>.mismatch.json for review, whether or not it is sent, and reporting never changes the release command's outcome or exit code. Set NIMBUS_REPORT_URL=disabled to switch submission off entirely regardless of configuration, or point it at your own endpoint for staging.

In the VS Code and IntelliJ release surfaces, the preview dialogis the prompt. When a validate or deploy fails this way, the editor offers to review an anonymized report; choosing to review shows exactly the sanitized record that would be sent, plus the count of renamed identifiers, and asks Send report / Save locally only / Cancel, defaulting to not sending. The daemon builds and sanitizes the record — the editor never sanitizes client-side and never sees raw problem text in the consent path — so what the dialog displays is byte-for-byte what submission would post. Both editors show the same wording and the same three choices.

Exit codes

FlagDefaultDescription
20-Configuration, toolchain, receipt, or preflight failure
21-Nimbus local validation, test, coverage, or mutation gate failure
22-Salesforce validation failure
23-Salesforce deployment failure
24-Production confirmation required or cancelled
25-Release integrity violation — the bundle is tampered, digest-mismatched, or missing (distinct from a config typo so CI can alert on tamper)
26-Deploy submitted to the org but its outcome could not be confirmed (lost contact) — the job runs on; resolve it with nimbus release watch <job>. NOT a deploy failure

nimbus assurance Pro

Feature overview: the Assurance console.

The Assurance console: a self-hosted web view of every release's signed evidence, for the whole team — including the people who read releases but never open a terminal. Your code and receipts never leave your infrastructure: Nimbus hosts nothing, the console reads local receipts and is read-only, and it never contacts a Salesforce org and never deploys. The only outbound connection is Nimbus's own licence check on startup — no code, no receipts, no org data — the same as any Nimbus command.

bash
nimbus assurance                      # open the console for this project
nimbus assurance --addr 0.0.0.0:8090  # share it with the team on your network

Each release shows, in plain language: who validated it, who deployed it, whether the checks passed (tests, coverage, static analysis), whether it drifted, its change reference, and — verified live in the browser — whether the bundle and signatures are intact. A dashboard summarises the set: production deploys, signed vs unsigned, counter-signed, and anything that failed verification. The verification is the same path release verify runs, so a tampered bundle or a forged signer name reads identically in the console and on the command line.

Access. On localhost the console is open — it's your machine. The moment you bind beyond loopback it requires an access token, so an evidence server reachable on the network is never wide-open: pass --tokenor set NIMBUS_ASSURANCE_TOKEN for a stable token, or let a network bind generate a per-session one and print a ready-to-open URL. The console (the page) is always served; only the evidence behind it is gated.

Evidence export and offline re-verification

nimbus release export writes the verified receipt set plus the signer roster into one self-verifying JSON document — the file you archive to your own immutable / WORM store for retention. It carries no source, no org aliases, and no credentials: it is evidence, not payload. The console offers the same as an export evidence download.

bash
nimbus release export --org-kind production --since 2026-07-01 --output evidence-2026Q3.json
nimbus release verify-export evidence-2026Q3.json                        # re-check every signature, offline
nimbus release verify-export --require-signed evidence-2026Q3.json       # CI gate: fail unless fully signed
nimbus release verify-export --roster .nimbus/release-signers.json ev.json # prove authenticity vs a trusted roster

nimbus release verify-export re-checks every signature and counter-signature against the roster embedded in the export — fully offline, on any machine, years later. It needs no Nimbus licence: an auditor who holds the file can re-verify it with no vendor relationship (authoring an export stays a Pro workflow; re-checking one is free). It exits 25 if any signature fails. Bundle integrity isn't re-checkable from an export (it carries no bundles), but each receipt's signed core — what was validated, and by which key — re-verifies forever.

A bare run reports what it finds and only fails on a signature that verifies incorrectly, so an empty or all-unsigned file still exits 0. Add --require-signed to turn it into a gate: it fails (25) unless the set is non-empty and every receipt carries a valid signature — the form to wire into a retention-integrity check.

Internal consistency vs. authenticity

By default verify-export checks each signature against the roster embedded in the same file — it answers "are these signatures internally consistent?", which is necessary but is not proof of provenance: a file could carry its own roster. To prove authenticity, pin an external trust anchor you already trust — the project's committed .nimbus/release-signers.jsonvia --roster, or a specific --expect-signer <keyId>. Any signature by a key outside the anchor is reported and fails (25). The anchor, not the file, becomes the root of trust.

What a signature attests — validation, not the deploy

The validation signature covers the immutable core (digests, gates, target, toolchain) fixed at validate time — not the status, actor, or transition trail, which change afterward at deploy time. So a succeededstatus is only proven when a deploy counter-signature backs it; otherwise it is self-reported. verify-export and the console label a deploy that is not counter-signed as self-reported, so a green "signature verified" is never mistaken for proof of who deployed, or when. Turn on requireCountersigned to make the deploy attestable (and, on production, enforce a second identity — exit 29).

Reading a receipt as change-management evidence

A signed receipt maps directly onto the assertions a change-management review asks for — so an auditor can read one as evidence:

Control assertionReceipt evidence
Authorizationthe change-ref (in the signed core) links the release to its ticket; the validation signature names who authorized it
Testing / quality gatesgates: tests, coverage, mutation score, static-analysis findings by severity — all in the signed core
Segregation of dutiesthe deploy counter-signature, by a different identity than the validator on production (exit 29 enforces it)
Integritythe sha256 bundle digest and the Ed25519 signatures — verify-export re-checks them offline
Completenessfailed and overridden releases keep their own receipts; the transition trail is timestamped and, once signed, tamper-evident (any edit fails verify)

CI/CD Integration

Nimbus runs without a Salesforce org, which means your CI pipeline has no JWT certificates, no DevHub dependency, and no scratch org pool to exhaust. Install the binary, run tests, collect output. See the CI/CD guide for full GitHub Actions, GitLab CI, SonarQube, and Codecov examples.

CI requires Pro Pro

Running nimbus test in a CI environment requires a Pro license. The Free tier is for local development only. Nimbus auto-detects GitHub Actions, GitLab CI, CircleCI, Travis, Jenkins, Buildkite, Azure Pipelines, Bitbucket, Drone, TeamCity, CodeBuild, AppVeyor, Semaphore, and any environment that sets CI=true.

Set NIMBUS_LICENSE_KEY as a secret in your CI platform. AI coding agents (Claude Code, Cursor, Aider, Codex, Continue) are detected and allowed to run Nimbus locally on the Free tier even if CI env vars leak through.

Sessions authenticated via NIMBUS_LICENSE_KEY register a machine on your license, just like nimbus login does. CI usage registers a single machine per CI provider (named CI — GitHub Actions etc.) no matter how many runners execute; an AI agent driving a local CLI registers that actual machine. Both appear in your portal and can be deactivated there.

After a Pro license validates, Nimbus records one privacy-minimal headless activation per license fingerprint, CI provider, and installation source. Repeated pipeline runs do not create additional activation rows. No license key, repository, branch, workflow, command arguments, or project content is sent. Set NIMBUS_TELEMETRY=false (or NIMBUS_NO_TRACKING=1) to disable it.

Pull requests from forks: GitHub does not expose repository secrets to pull_request runs that originate from a fork, so NIMBUS_LICENSE_KEY is empty there and the run fails with Set NIMBUS_LICENSE_KEY. Add a preflight step (shown below) so this surfaces clearly, and run tests on push or restrict the job to same-repo PRs.

Salesforce CLI Plugin

Install Nimbus through the Salesforce CLI and run the local runtime under the familiar sf command. The plugin installs the native Nimbus binary automatically on first use, verifies its release checksum, and preserves Nimbus output and exit codes.

bash
sf plugins install @nimbus-solution/nimbus-sf-plugin

sf nimbus login
sf nimbus test "*"
sf nimbus validate --sema error
sf nimbus deploy --target-org staging --source-dir force-app
sf nimbus release validate --release-profile production
sf nimbus exec --code "System.debug(1 + 1);"
sf nimbus mutate AccountService --min-score 80
sf nimbus doctor

The plugin uses the same Nimbus account and ~/.nimbus/license.json as the native CLI. Run sf nimbus login once to activate Pro features on the machine, sf nimbus whoami to inspect the active plan, andsf nimbus logout to deactivate the machine and free its license slot. This login is separate from Salesforce org authentication and survives runtime updates.

Salesforce-style Apex aliases are available when they fit existing CLI muscle memory:

bash
sf nimbus apex run test AccountServiceTest --coverage
sf nimbus apex validate --json
sf nimbus apex run --code "System.debug('local');"

Every native Nimbus flag passes through unchanged. Use sf nimbus installto update the plugin-managed runtime, --version to pin a release, or set NIMBUS_BINARY_PATH to run a development build.

Native nimbus sf is intentionally not mirrored assf nimbus sf: from inside Salesforce CLI, run the desiredsf command directly.

bash
sf nimbus install
sf nimbus install --version 1.7.0
sf nimbus version

GitHub Actions

Minimal working example. Set NIMBUS_PROFILE=ci to activate CI-specific settings from your nimbus.properties file.

yaml
name: Apex Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: nimbus-solution/setup-nimbus@v1

      - name: Check license secret
        # Fails clearly if the secret is missing or withheld (fork PR),
        # instead of a confusing "Set NIMBUS_LICENSE_KEY" later.
        env:
          NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
        run: |
          if [ -z "$NIMBUS_LICENSE_KEY" ]; then
            echo "::error::NIMBUS_LICENSE_KEY is empty — secret not set or withheld (fork PR)."
            exit 1
          fi

      - name: Run tests
        run: |
          nimbus test \
            --coverage --coverage-report coverage.xml \
            --results-xml results.xml
        env:
          NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
          NIMBUS_PROFILE: ci

      - name: Publish test results
        uses: EnricoMi/publish-unit-test-result-action@v2
        if: always()
        with:
          files: results.xml

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: coverage.xml
          token: ${{ secrets.CODECOV_TOKEN }}

For GitLab CI, SonarQube integration, external database setup, and caching, see the full CI/CD guide.

Release in CI Pro

The assured release workflow is two commands with a human decision between them, and it is built to run across two separate CI jobs.nimbus release validate runs the local gates and a Salesforce check-only validation, then writes an immutable receipt (.nimbus/releases/<releaseId>.json) and its content-addressed bundle. A later nimbus release deploy deploys that exact receipt — it does not re-validate. It reuses the validated org-side job where Salesforce supports quick-deploy and otherwise re-sends the stored bundle, so the deployed bytes are always the validated bytes.

The split is deliberate: validate and deploy run in different jobs on different runners, so a platform approval can sit between them. The receipt and bundle travel as a pipeline artifact — uploaded after validate, downloaded unchanged into the gated deploy job. The receipt is self-verifying: at deploy time Nimbus checks the org fingerprint, the quick-deploy expiry, the bundle digest, and the toolchain version policy, and fails closed on any drift. That verification is the safety net for the artifact hand-off.

The human gate is your platform's own environment protection — GitHub required reviewers, GitLab manual jobs, an Azure Environment approval check, or a shell prompt. Nimbus's --confirm-production is not that approval; it is the non-interactive acknowledgement that the gate was already passed, needed because a CI runner has no terminal to type the confirmation into.

Ready-to-copy templates for the four major platforms:

  • GitHub Actions — a gated environment: production deploy job.
  • GitLab CI/CD — a when: manual deploy on a protected environment.
  • Azure DevOps — a deployment job to an Environment with an approval check.
  • Generic shell — a portable two-phase script for self-hosted runners.

All four read the same ci release profile and pin the Salesforce CLI with nimbus toolchain sf install --version + nimbus toolchain sf status, so the runner provably reproduces the configured toolchain. See therelease-in-CI guidefor the secrets, the profile keys, and the exit-code contract.

Coverage Reports

Feature overview: code coverage.

Nimbus auto-detects the coverage format from the file extension passed to --coverage-report.

Console summary (Free)

bash
nimbus test --coverage

JSON - custom tooling, dashboards (Free)

bash
nimbus test --coverage --coverage-report coverage.json

HTML - visual report, browsable in your browser (Pro)

bash
nimbus test --coverage --coverage-report coverage.html
open coverage.html

XML Outputs Pro

XML formats integrate with SonarQube, Codecov, GitHub quality gates, and CI dashboards. Both flags can be combined in the same run.

Cobertura XML - coverage for SonarQube / Codecov / GitHub Actions

bash
nimbus test --coverage --coverage-report coverage.xml

JUnit XML - test results for CI dashboards and PR annotations

bash
nimbus test --results-xml results.xml

Both together

bash
nimbus test \
  --coverage --coverage-report coverage.xml \
  --results-xml results.xml